Memorizzare Elenchi di Valori con Vettori
Il primo tipo di collezione che esamineremo è Vec<T>
, nota anche come
vector. I vettori consentono di memorizzare più di un valore in un’unica
struttura dati che mette tutti i valori uno accanto all’altro in memoria. I
vettori possono memorizzare solo valori dello stesso type. Sono utili quando
si ha un elenco di elementi, come le righe di testo in un file o i prezzi degli
articoli in un carrello.
Creare un Nuovo Vettore
Per creare un nuovo vettore vuoto, chiamiamo la funzione Vec::new
, come
mostrato nel Listato 8-1.
fn main() { let v: Vec<i32> = Vec::new(); }
i32
Nota che qui abbiamo aggiunto un’annotazione di type. Poiché non stiamo
inserendo alcun valore in questo vettore, Rust non sa che tipo di elementi
intendiamo memorizzare. Questo è un punto importante. I vettori vengono
implementati utilizzando i type generici; spiegheremo come utilizzare i type
generici quando si creano type propri nel Capitolo 10. Per ora, sappiamo che
il type Vec<T>
fornito dalla libreria standard può contenere qualsiasi
type. Quando creiamo un vettore per contenere uno specifico type, possiamo
specificarlo tra parentesi angolari. Nel Listato 8-1, abbiamo detto a Rust che
Vec<T>
in v
conterrà elementi di type i32
.
Più spesso, si crea un Vec<T>
con valori iniziali e Rust dedurrà il type dai
valori che si desidera memorizzare, quindi raramente è necessario eseguire
questa annotazione di type. Rust fornisce opportunamente la macro vec!
, che
creerà un nuovo vettore contenente i valori che gli vengono assegnati. Il
Listato 8-2 crea un nuovo Vec<i32>
che contiene i valori 1
, 2
e 3
. Il
type intero è i32
perché è il type intero predefinito, come discusso nella
sezione “Tipi di Dato” del Capitolo 3.
fn main() { let v = vec![1, 2, 3]; }
Poiché abbiamo assegnato valori iniziali i32
, Rust può dedurre che il type
di v
è Vec<i32>
e l’annotazione di type non è necessaria. Successivamente,
vedremo come modificare un vettore.
Aggiornare un Vettore
Per creare un vettore e aggiungervi elementi, possiamo usare il metodo push
,
come mostrato nel Listato 8-3.
fn main() { let mut v = Vec::new(); v.push(5); v.push(6); v.push(7); v.push(8); }
push
per aggiungere valori a un vettoreCome per qualsiasi variabile, se vogliamo poterne modificare il valore, dobbiamo
renderla mutabile usando la parola chiave mut
, come discusso nel Capitolo 3. I
numeri che inseriamo al suo interno sono tutti di type i32
, e Rust lo deduce
dai dati, quindi non abbiamo bisogno dell’annotazione Vec<i32>
.
Leggere Elementi dei Vettori
Esistono due modi per fare riferimento a un valore memorizzato in un vettore:
tramite indicizzazione o utilizzando il metodo get
. Negli esempi seguenti,
abbiamo annotato i type dei valori restituiti da queste funzioni per maggiore
chiarezza.
Il Listato 8-4 mostra entrambi i metodi per accedere a un valore in un vettore,
con la sintassi di indicizzazione e il metodo get
.
fn main() { let v = vec![1, 2, 3, 4, 5]; let terzo: &i32 = &v[2]; println!("Il terzo elemento è {terzo}"); let terzo: Option<&i32> = v.get(2); match terzo { Some(terzo) => println!("Il terzo elemento è {terzo}"), None => println!("Non c'è un terzo elemento."), } }
get
per accedere a un elemento in un vettoreSono da notare alcuni dettagli. Utilizziamo il valore di indice 2
per ottenere
il terzo elemento poiché i vettori sono indicizzati per numero, a partire da
zero. Utilizzando &
e []
otteniamo un reference all’elemento a
quell’indice. Quando utilizziamo il metodo get
con l’indice passato come
argomento, otteniamo un Option<&T>
che possiamo utilizzare con match
.
Rust fornisce questi due modi per ottenere un reference ad un elemento, in modo da poter scegliere come il programma si comporta quando si tenta di utilizzare un valore di indice al di fuori dell’intervallo di elementi esistenti. Ad esempio, vediamo cosa succede quando abbiamo un vettore di cinque elementi e poi proviamo ad accedere a un elemento all’indice 100 con ciascuna tecnica, come mostrato nel Listato 8-5.
fn main() { let v = vec![1, 2, 3, 4, 5]; let non_esiste = &v[100]; let non_esiste = v.get(100); }
Quando eseguiamo questo codice, il primo metodo []
causerà un crash del
programma perché fa riferimento ad un elemento inesistente. Questo metodo è
ideale quando si desidera che il programma si blocchi in caso di tentativo di
accesso a un elemento oltre la fine del vettore.
Quando al metodo get
viene passato un indice esterno al vettore, restituisce
None
senza crash. Si consiglia di utilizzare questo metodo se l’accesso a un
elemento oltre l’intervallo del vettore può verificarsi occasionalmente in
circostanze normali. Il codice avrà quindi una logica per gestire sia
Some(&element)
che None
, come discusso nel Capitolo 6. Ad esempio, l’indice
potrebbe provenire da un utente che inserisce un numero. Se inserisce
accidentalmente un numero troppo grande e il programma ottiene un valore None
,
è possibile indicare all’utente quanti elementi sono presenti nel vettore
corrente e dargli un’altra possibilità di inserire un valore valido. Sarebbe più
intuitivo che mandare in crash il programma a causa di un errore di battitura!
Quando il programma ha un reference valido, il sistema applica le regole di ownership e borrowing (trattate nel Capitolo 4) per garantire che questo reference e qualsiasi altro reference al contenuto del vettore rimangano validi. Ricordati la regola che stabilisce che non è possibile avere reference mutabili e immutabili nello stesso scope. Questa regola si applica al Listato 8-6, dove manteniamo un reference immutabile al primo elemento di un vettore e proviamo ad aggiungere un elemento alla fine. Questo programma non funzionerà se proviamo a fare reference a quell’elemento anche più avanti nella funzione.
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let primo = &v[0];
v.push(6);
println!("Il primo elemento è: {primo}");
}
La compilazione di questo codice genererà questo errore:
$ cargo run
Compiling collections v0.1.0 (file:///progetti/collections)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:7:5
|
5 | let primo = &v[0];
| - immutable borrow occurs here
6 |
7 | v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
8 |
9 | println!("Il primo elemento è: {primo}");
| ----- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections` (bin "collections") due to 1 previous error
Il codice nel Listato 8-6 potrebbe sembrare funzionante: perché un reference al primo elemento dovrebbe preoccuparsi delle modifiche alla fine del vettore? Questo errore è dovuto al funzionamento dei vettori: poiché i vettori posizionano i valori uno accanto all’altro in memoria, se non c’è abbastanza spazio per posizionare tutti gli elementi uno accanto all’altro dove il vettore è attualmente memorizzato l’aggiunta di un nuovo elemento alla fine del vettore potrebbe richiedere l’allocazione di nuova memoria e la copia dei vecchi elementi nel nuovo spazio. In tal caso, il reference al primo elemento punterebbe alla memoria de-allocata. Le regole di borrowing impediscono ai programmi di finire in questa situazione.
Nota: per maggiori dettagli sull’implementazione del type
Vec<T>
, vedere “Il Rustonomicon”.
Iterare sui Valori di un Vettore
Per accedere a ogni elemento di un vettore a turno, dovremmo iterare su tutti
gli elementi anziché utilizzare gli indici per accedervi uno alla volta. Il
Listato 8-7 mostra come utilizzare un ciclo for
per ottenere reference
immutabili a ciascun elemento in un vettore di valori i32
e stamparli.
fn main() { let v = vec![100, 32, 57]; for i in &v { println!("{i}"); } }
for
Possiamo anche iterare su reference mutabili a ciascun elemento in un vettore
mutabile per apportare modifiche a tutti gli elementi. Il ciclo for
nel
Listato 8-8 aggiungerà 50
a ciascun elemento.
fn main() { let mut v = vec![100, 32, 57]; for i in &mut v { *i += 50; } }
Per modificare il valore a cui punta il reference mutabile, dobbiamo usare
l’operatore di de-referenziazione *
per arrivare al valore in i
prima di
poter usare l’operatore +=
. Approfondiremo l’operatore di de-referenziazione
nella sezione “Seguire il Reference al Valore” del
Capitolo 15.
L’iterazione su un vettore, sia immutabile che mutabile, è sicura grazie alle
regole di ownership e borrowing. Se tentassimo di inserire o rimuovere
elementi nei corpi del ciclo for
nei Listati 8-7 e 8-8, otterremmo un errore
del compilatore simile a quello ottenuto con il codice nel Listato 8-6. Il
reference al vettore contenuto nel ciclo for
impedisce la modifica
simultanea dell’intero vettore.
Utilizzare un’Enum per Memorizzare Più Type
I vettori possono memorizzare solo valori dello stesso type. Questo può essere scomodo; ci sono sicuramente casi d’uso in cui è necessario memorizzare un elenco di elementi di type diverso. Fortunatamente, le varianti di un’enum sono definite sotto lo stesso type di enum, quindi quando abbiamo bisogno di un type per rappresentare elementi di tipi diversi, possiamo definire e utilizzare un’enum!
Ad esempio, supponiamo di voler ottenere valori da una riga di un foglio di calcolo in cui alcune colonne della riga contengono numeri interi, alcuni numeri in virgola mobile e alcune stringhe. Possiamo definire un’enum le cui varianti conterranno i diversi tipi di valore, e tutte le varianti dell’enum saranno considerate dello stesso type: quello dell’enum. Possiamo adesso creare un vettore per contenere quell’enum e quindi, in definitiva, contenere type “diversi”. Lo abbiamo dimostrato nel Listato 8-9.
fn main() { enum CellaFoglioDiCalcolo { Int(i32), Float(f64), Text(String), } let row = vec![ CellaFoglioDiCalcolo::Int(3), CellaFoglioDiCalcolo::Text(String::from("blu")), CellaFoglioDiCalcolo::Float(10.12), ]; }
Rust deve sapere quali type saranno presenti nel vettore in fase di
compilazione, in modo da sapere esattamente quanta memoria nell’heap sarà
necessaria per memorizzare ogni elemento. Dobbiamo anche essere espliciti sui
type consentiti in questo vettore. Se Rust permettesse a un vettore di
contenere qualsiasi type, ci sarebbe la possibilità che uno o più type
causino errori nelle operazioni eseguite sugli elementi del vettore. L’utilizzo
di un’enum assieme ad un’espressione match
significa che Rust garantirà in
fase di compilazione che ogni possibile caso venga gestito, come discusso nel
Capitolo 6.
Se non si conosce l’insieme esaustivo di type che un programma avrà una volta in esecuzione da memorizzare in un vettore, la tecnica dell’enum non funzionerà. In alternativa, è possibile utilizzare un oggetto trait, che tratteremo nel Capitolo 18.
Ora che abbiamo discusso alcuni dei modi più comuni per utilizzare i vettori,
assicurati di consultare la documentazione dell’API
per tutti i numerosi metodi utili definiti su Vec<T>
dalla libreria standard.
Ad esempio, oltre a push
, un metodo pop
rimuove e restituisce l’ultimo
elemento.
Eliminare un Vettore Elimina i Suoi Elementi
Come qualsiasi altra struct
, un vettore viene rilasciato quando esce dallo
scope, come annotato nel Listato 8-10.
fn main() { { let v = vec![1, 2, 3, 4]; // esegui operazioni su `v` } // <- qui `v` esce dallo scope e la memoria viene liberata }
Quando il vettore viene rilasciato, anche tutto il suo contenuto viene de-allocato, il che significa che gli interi in esso contenuti verranno de-allocati. Il borrow checker (controllo dei prestiti) garantisce che qualsiasi reference al contenuto di un vettore venga utilizzato solo finché il vettore stesso è valido.
Passiamo al tipo di collezione successivo: String
!