Home Rhyylen
Contatto
 
 
 
Rust Language
Capitolo 22
I vettori

I vettori possono essere considerati per molti aspetti come i fratelli maggiori degli array. In entrambi i casi memorizziamo collezioni di elementi dello stesso tipo in maniera indicizzata ma le differenze sono notevoli. In pratica queste si possono riassumere come segue:
 
Dimensione:
Gli array hanno una dimensione fissa che deve essere nota al momento della compilazione e non può essere modificata in seguito.
I vettori, invece, sono dinamici e possono crescere o diminuire durante l’esecuzione del programma. Il formalismo che li definisce è Vec<T> che ci conferma che essi sono costituiti da elementi tutti appartenenti allo stesso tipo, come detto all'inizio
Tipo di Allocamento:
Gli array sono allocati sullo stack, e la loro dimensione deve essere nota a tempo di compilazione.
I vettori sono allocati sullo heap, permettendo loro di avere una dimensione variabile e di espandersi all’occorrenza.
Sintassi:
Esistono differenze a livello dichiarativo. Gli array abbiamo visto nel capitolo 13 come possano essere definitivi, qui ilustreremo i vari sistemi possibili con i vettori e ci renderemo conto delle differenze.
Performance:

Gli array possono offrire prestazioni leggermente migliori per via della loro allocazione sullo stack.
I vettori possono avere un overhead di prestazioni dovuto alla loro natura dinamica e alla gestione della memoria sull’heap.
Uso:
Gli array sono ideali quando si conosce la dimensione del dataset e questa non è destinata a cambiare.
I vettori sono più adatti quando si ha bisogno di una collezione che può cambiare dimensione in pratica quando serve uno strumento duttile grazie alla sua variabilità.

Come avrete forse già intuito, anche i vettori sono indicizzati, in maniera sequenziale tramite interi partendo da 0.
Partiamo quindi con un esempio di base:


let v1:  Vec<i32> = Vec::new();

che crea un vettore vuoto. Il tipo, deve essere specificato, l'inferenza, non essendoci elementi, non può intervenire.
Diversamente possiamo inserire direttamente dei valori usando la macro vec![elementi]:

let v1 = vec![1,2,3];

che, in questo caso, crea un array di i32, il default per gli interi, come noto.

Un altro sistema è il seguente che crea un vettore di N elementi X tutti uguali è la seguente:

let v1 = vec![X; N]

e su questa base potrete anche scrivere una sequenza di istruzioni così:

let x = 4;
let v1 = vec![1;x];

cosa che non è possibile con gli array la cui dimensione deve essere a nota compile time. Ma con i vettori siamo di fronte ad una struttura dinamica. E' invece interessante notare se specificate nella prima riga:

let x: i32 = 4;

il compilatore si lamenta. Questo perchè l'indice dei vettori, esattamente come per gli array, è di tipo usize.

Quelli che abbiamo visto sono metodi base per creare vettori diciamo ex-novo, vedremo più avanti in questo paragrafo come crearli a partire da altre strutture dati.
I vettori sono indicizzati e per accedere ed un singolo elemento possiamo ricorrere all'operatore
[x] laddove x rappresenta l'indice che ci interessa estrarre. Alla fine del paragrafo vedremo comunque un altro metodo interessante. Nonostante siano proni ad essere modificati, anche per essi Rust, giustamente, pretende che, se devono essere sottoposti a variazioni, siano dichiarati con la keyword mut. Vediamo quindi qualche piccolo esempio al fine di apprendere la manipolazione di queste importanti strutture. Come avrete intuito l'operatore [x] vi permette anche di sostituire un elemento con un altro (dello stesso tipo, si capisce) quindi per esempio:

let mut v = vec![1, 2, 3, 4, 5];
v[0] = 77;
println!("{:?}", v);

mentre  molto utili risultano due metodi ovvero
push(elemento) e pop() che rispettivamente aggiungono e tolgono un elemento in coda al vettore.

  Esempio 22.1
1
2
3
4
5
6
7
8
9
fn main() {
    let mut v1: Vec<i32> = Vec::new();
    v1.push(0);
    v1.push(3);
    v1.push(5);
    println!("{:?}", v1);
    v1.pop();
    println!("{:?}", v1);
}

che ci presenta il seguente output:

[0, 3, 5]
[0, 3]

direi abbastanza semplice da capire.
Altri tre metodi molto usati sono
- insert(indice, elemento) che inserisce un elemento un certa posizione
- remove(indice) che rimuove l'elemento che si trova alla posizione indicata dal parametro indice.
- len() che riporta il numero di elementi del vettore.
Vediamo un esempio riepilogativo:

  Esempio 22.2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn main(){
    let mut v1: Vec<i32> = Vec::new();
    v1.push(4);
    v1.push(5);
    v1.push(0);
    v1.push(7);
    for x in 0..v1.len() {
    println!("elemento {} = {}", x, v1[x]);
    }
    println!("---------");
    v1.insert(2, 9);
    for x in 0..v1.len() {
        println!("elemento {} = {}", x, v1[x]);
    }
    println!("---------");
    v1.remove(2);
    v1.remove(3);
    v1.pop()

    for x in 0..v1.len() {
        println!("elemento {} = {}", x, v1[x]);
    }
}

A valle di tutto troviamo i consueti meccanismi di controllo di Rust che non permettono di scrivere in zone di memoria non presidiate. Un esempio banale è il seguente:

let mut v1 = vec![1;4];    
v1[6] = 7;

questo codice origina un errore:

thread 'main' panicked at r315.rs:3:7:
index out of bounds: the len is 4 but the index is 6

ovvero il programma origna un stato di "panico" (di cui parleremo in apposito paragrafo) e blocca l'esecuzione in quanto stiamo cercando di scrivere alla posizione 6 in un vettore di 4 elementi, quindi stiamo andando fuori indice.
Stessa cosa avverrebbe con insert, ad esempio se la seconda istruzione del frammento precedente fosse:

v1.insert(5,9)

ne otterremmo:

thread 'main' panicked at /rustc/25ef9e3d85d934b27d9dada2f9dd52b1dc63bb04\library\alloc\src\vec\mod.rs:1522:21:
insertion index (is 5) should be <= len (is 4)

anche qui una indicazione molto chiara. Rust dispone davvero di un eccellente compilatore in termini di interazione con l'utente. Cosa del tutto analoga avviene se provate a rimuovere un elemento fuori indice.
Nell'esempio ... avrete notato che abbiamo attraversato gli elementi di un vettore utilizzando un semplice ciclo for che itera in un range i cui estremi sono 0 e il numero di elementi di un vettore-1 (ricordiamoci che iniziano da 0). Si tratta di un sistema molto comodo ma ce ne sono altri:

attaverso iter()

let v1 = vec![1,2,3,4];
for x in v1.iter() {
println!("{}", x);
}

usando un puntatore:

let v1 = vec![1,2,3,4];
for x in &v1 {
println!("{}", x);
}

o anche senza puntatore:

let v1 = vec![1,2,3,4];
for x in v1 {
println!("{}", x);
}


Negli ultimi due casi c'è una sostanziale differenza, come potrete ormai intuire. Nel penultimo il vettore è ancora utilizzabile al di fuori del for mentre nell'ultimo l'iteratore all'interno del ciclo prende possesso (ownership, come sempre!) del vettore e alla chiusura del for avviene il drop dello stesso col risultato che al di fuori non esiste più. In pratica:

fn main(){
    let v1 = vec![1,2,3,4];
    for x in v1 {
        println!("{}", x);
    }
    println!("{:?}", v1)
}

questo codice produce questo errore:

error[E0382]: borrow of moved value: `v1`
--> r322.rs:6:26
|
2 | let v1 = vec![1,2,3,4];
| -- move occurs because `v1` has type `Vec<i32>`, which does not implement the `Copy` trait
3 | for x in v1 {
| -- `v1` moved due to this implicit call to `.into_iter()`
...
6 | println!("{:?}", v1)
| ^^ value borrowed here after move

Che ci spiega insomma le solite cose.

Un altro caso che può presentarsi è presente anche nella documentazione ufficiale e ci aiuta a capire meglio il funzionamento dei vettori e in generali dell'approccio di Rust alla gestione della memoria.

Abbiamo detto che è possibile creare un vettore an che partendo da altre fonti, ovvero da altre strutture dati già create. Questo è certamente un caso molto comune.

Un primo modo è quello che permette di passare da un array ad un vettore, comodo se dovete passare da una struttura statica ad una dinamica:

fn main() {
    let array = [1, 2, 3, 4, 5];
    let vect = array.to_vec();
    println!("Vettore da array: {:?}", vect);
}

un secondo prevede il passaggio ad esempio da un range, che come sappiamo è molto veloce da generare, attraverso collect, come avevamo anticipato nel capitolo dedicato prorpio ai range. Si fa uso di un metodo molto potente e versatile ovvero collect. Questo metodo merita un approdondimento, come ci consiglia anche la sua sintassi che troviamo nell'esempio di seguito:

fn main() {
    let range = 0..6; // Crea un range da 0 a 5
    let vect = range.collect::<Vec<_>>();
    println!("Vettore da range: {:?}", vect);
}

la sintassi ::<elemento> è nota come "turbofish" per via della sua forma... (va beh...) ed è un aiuto che diamo all'inferenza per determinare su cosa deve lavorare. Fornisce quindi pieno controllo al programmatore  sul tipo da utilizzare. In aggiunta abbiamo Vec<-> con l'elemento underscore all'interno della coppia <> che è un segnaposto per indicare che all'interno vi sarà un tipo di elemento che il compilatore potrà determinare da sè.
Se volete, una maniera un po' più chiara per passare da range a Vect, possiamo scrivere:


let numeri: Vec<_> = (0..10).collect();

Un caso un po' più complesso si ha qualora voleste passare da una ennupla ad un vettore. Bisogna infatti tenere presente che la ennupla può contenere valori diversi come tipologia mentre un vettore no e quindi bisogna trovare un tipo univoco in cui riversare gli elementi della ennupla il che obbliga alla destrutturazione della ennupla stessa come nel seguente esempio:

fn main() {
let tupla = (1, "due", 3.0);
let vect = vec![tupla.0.to_string(), tupla.1.to_string(), tupla.2.to_string()];
println!("Vettore da tupla: {:?}", vect);
}

I metodi di gestione dei vettori sono tanti, come sempre ne vediamo alcuni che possono essere utili per lavorare in maniera efficace da subito:

resize(dimensione, elemento) che può estendere un vettore inserendo valori nuovi oppure troncare lo stesso se la nuova dimensione è inferiore alla precedente.
 
let mut v1 = vec![1,2,3];
v1.resize(5, 9);
println!("{:?}", v1);
v1.resize(1,0);
println!("{:?}", v1);

clear() ripulisce il vettore e is_empty() ci può conferma il successo di quella operazione

let mut v1 = vec![1,2,3];
v1.clear();
println!("{}", v1.is_empty());

append vi permetterà di unire due vettori. Attenzione che la copia di per sè è distruttiva, ovvero dopo il processo il vettore che è andato ad integrare l'altro risulta vuoto. Vediamo un esempio che ci dice anche altre cose interessanti:

fn main() {
    let mut vect1 = vec![1, 2, 3];
    let mut vect2 = vec![4, 5, 6];
    // Aggiunge gli elementi di vect1 alla fine di vect2
    vect2.append(&mut vect1);
    // Dopo l'operazione, vect1 sarà vuoto
    println!("vect1: {:?}", vect1); // Stampa "vect1: []"
    println!("vect2: {:?}", vect2); // Stampa "vect2: [4, 5, 6, 1, 2, 3]"
}

Avrete notato che l'istruzione che esegue il metodo è append(&mut vect1) e non semplicemente append(vect1)... cosa significa.. che quello che viene passato è un riferimento mutabile che permette di svuotare il vettore che fa a completare il primo che è quello che fa append(). Se volete preservare il vettore originale dovrete usare clone() su vect1 quando lo appendete. L'altra scrittura rappresenterebbe un passaggio per valore, valore che sarebbe quindi di proprietà di vect1 e vect2. Rust non lo permette.
Volendo si può usare anche
extend, che ha un carattere più generale e non distrugge niente se ovviamente usato nel formato che segue:

let mut vec1 = vec![1, 2, 3];    
let vec2 = vec![4, 5, 6];    
vec1.extend(vec2.clone());


Anche with_capacity(s:usize) vi potrà aiutare ad ottenere performance stabili predefinendo lo spazio da riservare ai vostri vettori:

let mut vec = Vec::with_capacity(10);

L'unico rischio è lo spreco di memoria se esagerate concedendo capacità non necessarie.

Questi sono solo alcuni esempi, ma di metodi da utilizzare ce ne sono davvero molti, sul sito ufficiale li trovate ovviamente tutti.


Il solito problema della copia di un vettore in un altro può essere risolto tramite i metodo clone() o tramite clone_from(). Iniziamo col dire che Vect non implementa il trait Copy per cui:

let mut v1 = vec![1,2,3];
let mut v2 = v1;
println!("{:?}", v1);

non compila e quindi niente strada facile con l'operatore =
Proviamo quindi un'altra strada e, condizione che T implementi il trait clone, possiamo scrivere

let vec_originale = vec![1, 2, 3];
let vec_copia = vec_originale.clone();

che crea ex novo una copia indipendente del vec_originale.

L'altro metodo che possiamo adottare fa uso di clone_from() che è un metodo che permette di ottimizzare il processo di clonazione quando già si possiede un oggetto destinatario. clone_from() modifica l'oggetto su cui viene chiamato per renderlo una copia dell'oggetto passato come parametro, utilizzando se possibile le risorse già allocate dall'oggetto destinatario per minimizzare le nuove allocazioni. In questo senso garantisce maggiore efficienza.

fn main() {
  let mut v1 = vec![1,2,3];
  let mut v2 = vec![1;3];
  v2.clone_from(&v1);
  println!("{:?}", v1);
  println!("{:?}", v2);
  v1[0] = 9;
  println!("{:?}", v1);
  println!("{:?}", v2);
  v2[0] = 88;
  println!("{:?}", v1);
  println!("{:?}", v2);
}

Ecco un po' di codice che dimostra anche l'indipendenza tra copie ed originali, l'output infatti è:

[1, 2, 3]
[1, 2, 3]
[9, 2, 3]
[1, 2, 3]
[9, 2, 3]
[88, 2, 3]

Relativamente all'accesso al singolo elemento abbiamo detto che l'operatore [] è quello designato abitualmente. Ma Rust ci propone una strada alternativa che fa uso di get(indice) proprio come abbiamo visto per gli array nel capitolo 13. La particolarità, anche qui, di questa funzione è che non restituisce un indice ma un tipo Option (vedere capitolo) il che ci permette di evitare situazioni panico. Vediamo infatti i seguenti due programmi:

fn main() {    
    let v = vec![1, 2, 3, 4, 5];    
    let r = v[8];    
    print!("{}", r);
}


questo programma compila ma ne risulta un errore a runtime:

thread 'main' panicked at r348.rs:3:20:
index out of bounds: the len is 5 but the index is 8
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


In questo caso se viene selezionato un indice fuori dal range abbiano un crash (panico) da parte del programma. Vediamo una versione che fa uso di get:

fn main() {    
    let v = vec![1, 2, 3, 4, 5];    
    let r: Option<&i32> = v.get(8);    
    match r {        
    Some(r) => println!("{}", r),        
    None => println!("Fuori range"),    
    }
}

In questo caso il programma non va in crash ma ci dice semplicemente che siamo fuori range.
Come per gli array, abbiamo a disposizione
get_mut(indice) per sostituire un elemento in modo sicuro tramite la generazione di un tipo Option che ci aiuta a gestire situazioni anomale, potete rivedere l'esempio al .

Un'altra situazione interessante che possiamo illustrare grazie ai vect è la seguente, esempio classico presente anche sulla documentazione ufficiale che ci permette di disquisire un po' sulle caratteristiche e sulla filosofia del compilatore:

  Esempio 22.3
1
2
3
4
5
6
fn main() {
    let mut v = vec![1, 2, 3, 4, 5];
    let first = &v[0];
    v.push(6);
    println!("The first element is: {first}");
}

Questo programma non compila:

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> r353.rs:4:5
|
3 | let first = &v[0];
| - immutable borrow occurs here
4 | v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
5 | println!("The first element is: {first}");
| ------- immutable borrow later used here

Il compilatore stavolta è forse un po' ambiguo. Esso ci dice che abbiamo definito un borrow immutabile e uno mutabile. In realtà a far andare in crisi la compilazione è l'uso che viene fatto alla riga 5 dei dati prestati. In effetti se togliete la riga 5 stessa il processo di compilazione va a buon fine. Cosa succede:
alla riga 3 creiamo un borrow immutabile
alla riga 4 modifichiamo il vettore aggiungendo un elemento il che potrebbe portare ad una riallocazione del vettore stesso rendendo invalido l'altro riferimento.
Tutto ciò è innocuo fino alla riga 5 dove cerchiamo di utilizzare il primo riferimento. Se tale riga scompare non c'è motivo di bloccare la compilazione. Nota: in passato il compilatore era più rigido e bloccava la compilazione anche senza riga di stampa ma successivamente il borrow checker è stato aggiornato, si parla di Non-Lexical Lifetime, per consentire una esperienza di programmazione più libera e comunque sempre non rischiosa. Su queste cose sono un po' estremista e il primo approccio, quello più rigoroso, mi pareva più consono alla filosofia del linguaggio. Tuttavia pare che mantenere un livello di controllo  troppo rigido portasse pochi benefici o nulli e molti svantaggi. Dal 2018 è stato modificato il comportamento del compilatore e i risultati sono stati positivi e ben accolti dalla comunità.

Se modifichiamo il programma come segue alla riga 3:

let first = v[0];

ossia andiamo di trait Copy tutto gira, stampa o no.

Ultima informazione, più che altro di carattere culturale, da tenere presente, come da documentazione ufficiale, i vettori non allocheranno mai più di isize::Max bytes che è il loro limite di occupazione in memoria.
Per ora è tutto con i vettori, il prossimo step che li coinvolgerà riguarderà la loro convivenza con le funzioni.