SE VUOI PRENDERE LA CERTIFICAZIONE PER QUESTO CORSO CLICCA QUI
Archiviazione di Liste di Valori con Vettori
Il primo tipo di collezione che esamineremo è Vec<T>
, anche noto come vettore. I vettori ti consentono di archiviare più di un valore in una singola struttura dati che mette tutti i valori uno accanto all’altro in memoria. I vettori possono memorizzare solo valori dello stesso tipo. Sono utili quando hai una lista di elementi, come le righe di testo in un file o i prezzi degli articoli in un carrello della spesa.
Creazione di un Nuovo Vettore
Per creare un nuovo vettore vuoto, chiamiamo la funzione Vec::new
, come mostrato nell’Elenco 8-1.
rust
let v: Vec<i32> = Vec::new();
Elenco 8-1: Creazione di un nuovo vettore vuoto per contenere valori di tipo i32
Nota che abbiamo aggiunto una annotazione di tipo qui. Poiché non stiamo inserendo alcun valore in questo vettore, Rust non sa che tipo di elementi intendiamo memorizzare. Questo è un punto importante. I vettori sono implementati utilizzando i generics; affronteremo come utilizzare i generics con i tuoi tipi in Capitolo 10. Per ora, sappi che il tipo Vec<T>
fornito dalla libreria standard può contenere qualsiasi tipo. Quando creiamo un vettore per contenere un tipo specifico, possiamo specificare il tipo tra parentesi angolari. Nell’Elenco 8-1, abbiamo detto a Rust che il Vec<T>
in v
conterrà elementi del tipo i32
.
Più spesso, creerai un Vec<T>
con valori iniziali e Rust inferirà il tipo di valore che vuoi memorizzare, quindi raramente è necessaria questa annotazione di tipo. Rust fornisce convenientemente il macro vec!
, che creerà un nuovo vettore che contiene i valori che gli fornisci. L’Elenco 8-2 crea un nuovo Vec<i32>
che contiene i valori 1, 2 e 3. Il tipo intero è i32
perché è il tipo di intero predefinito, come discusso nella sezione “Tipi di Dati” del Capitolo 3.
rust
let v = vec![1, 2, 3];
Elenco 8-2: Creazione di un nuovo vettore contenente valori
Poiché abbiamo fornito valori i32
iniziali, Rust può dedurre che il tipo di v
è Vec<i32>
, e l’annotazione di tipo non è necessaria. Successivamente, vedremo come modificare un vettore.
Aggiornamento di un Vettore
Per creare un vettore e quindi aggiungere elementi ad esso, possiamo utilizzare il metodo push
, come mostrato nell’Elenco 8-3.
rust
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
Elenco 8-3: Utilizzo del metodo push
per aggiungere valori a un vettore
Come con qualsiasi variabile, se vogliamo poterne cambiare il valore, dobbiamo renderla mutabile utilizzando la parola chiave mut
, come discusso nel Capitolo 3. I numeri che inseriamo sono tutti di tipo i32
, e Rust lo deduce dai dati, quindi non abbiamo bisogno dell’annotazione Vec<i32>
.
Lettura degli Elementi dei Vettori
Ci sono 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 tipi dei valori restituiti da queste funzioni per maggiore chiarezza.
L’Elenco 8-4 mostra entrambi i metodi per accedere a un valore in un vettore, con la sintassi di indicizzazione e il metodo get
.
rust
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'è terzo elemento."),
}
Elenco 8-4: Utilizzo della sintassi di indicizzazione o del metodo get
per accedere a un elemento in un vettore
Nota alcuni dettagli qui. Usiamo il valore dell’indice 2 per ottenere il terzo elemento perché i vettori sono indicizzati per numero, a partire da zero. Utilizzando &
e []
otteniamo un riferimento all’elemento all’indice specificato. Quando usiamo il metodo get
con l’indice passato come argomento, otteniamo un Option<&T>
che possiamo utilizzare con match
.
Il motivo per cui Rust fornisce questi due modi per fare riferimento a un elemento è in modo che tu possa scegliere come deve comportarsi il programma quando si tenta di utilizzare un valore di indice al di fuori del range degli elementi esistenti. Come 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 nell’Elenco 8-5.
rust
let v = vec![1, 2, 3, 4, 5];
let non_esiste = &v[100];
let non_esiste = v.get(100);
Elenco 8-5: Tentativo di accedere all’elemento all’indice 100 in un vettore contenente cinque elementi
Quando eseguiamo questo codice, il primo metodo []
causerà il blocco del programma perché fa riferimento a un elemento inesistente. Questo metodo è meglio utilizzato quando vuoi che il tuo programma si blocchi se viene tentato l’accesso a un elemento oltre la fine del vettore.
Quando il metodo get
viene passato un indice che si trova al di fuori del vettore, restituisce None
senza bloccarsi. Useresti questo metodo se l’accesso a un elemento al di là del range del vettore può avvenire occasionalmente in circostanze normali. Il tuo codice avrà quindi la logica per gestire avendo sia Some(&elemento)
o None
, come discusso nel Capitolo 6. Ad esempio, l’indice potrebbe provenire da una persona che inserisce un numero. Se inseriscono accidentalmente un numero troppo grande e il programma riceve un valore None
, potresti dire all’utente quanti elementi ci sono nel vettore corrente e dargli un’altra possibilità di inserire un valore valido. Questo sarebbe più user-friendly rispetto al blocco del programma a causa di un errore di battitura!
Quando il programma ha un riferimento valido, il borrow checker applica le regole di proprietà e prestito (trattate nel Capitolo 4) per garantire che questo riferimento e qualsiasi altro riferimento ai contenuti del vettore rimangano validi. Ricorda la regola che stabilisce che non puoi avere riferimenti mutabili e immutabili nello stesso ambito. Quella regola si applica nell’Elenco 8-6, dove manteniamo un riferimento immutabile al primo elemento in un vettore e cerchiamo di aggiungere un elemento alla fine. Questo programma non funzionerà se proviamo anche a fare riferimento a quell’elemento più tardi nella funzione:
rust
let mut v = vec![1, 2, 3, 4, 5];
let primo = &v[0]; v.push(6);
println!("Il primo elemento è: {primo}");
Elenco 8-6: Tentativo di aggiungere un elemento a un vettore mantenendo un riferimento a un elemento
La compilazione di questo codice produrrà questo errore:
less
$ cargo run
Compilazione in corso per collections v0.1.0 (file:///projects/collections)
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:6:5
|
4 | let primo = &v[0];
| - prestito immutabile si verifica qui
5 |
6 | v.push(6);
| ^^^^^^^^^ prestito mutabile si verifica qui
7 |
8 | println!("Il primo elemento è: {primo}");
| ----- prestito immutabile usato successivamente qui
Per ulteriori informazioni su questo errore, prova `rustc --explain E0502`.
errore: non è stato possibile compilare `collections` a causa di un errore precedente
Il codice nell’Elenco 8-6 potrebbe sembrare dovrebbe funzionare: perché un riferimento al primo elemento dovrebbe preoccuparsi di modifiche alla fine del vettore? Questo errore è dovuto al modo in cui funzionano i vettori: perché i vettori mettono i valori accanto l’uno all’altro in memoria, 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, se non c’è abbastanza spazio per mettere tutti gli elementi accanto l’uno all’altro dove il vettore è attualmente memorizzato. In quel caso, il riferimento al primo elemento puntava a memoria dealloca senza più contenuto. Le regole di prestito impediscono ai programmi di trovarsi in quella situazione.
Nota: Per ulteriori informazioni sui dettagli di implementazione del tipo Vec<T>
, consulta “The Rustonomicon”.
Iterare sui Valori in un Vettore
Per accedere a ciascun elemento in un vettore a turno, iteriamo su tutti gli elementi anziché utilizzare gli indici per accedere uno alla volta. L’Elenco 8-7 mostra come utilizzare un ciclo for
per ottenere riferimenti immutabili a ciascun elemento in un vettore di valori i32
e stamparli.
rust
let v = vec![100, 32, 57];
for i in &v {
println!("{i}");
}
Elenco 8-7: Stampa di ciascun elemento in un vettore iterando sugli elementi utilizzando un ciclo for
Possiamo anche iterare su riferimenti mutabili a ciascun elemento in un vettore mutabile per apportare modifiche a tutti gli elementi. Il ciclo for
nell’Elenco 8-8 aggiungerà 50 a ciascun elemento.
rust
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
Elenco 8-8: Iterare su riferimenti mutabili agli elementi in un vettore
Per cambiare il valore a cui si riferisce il riferimento mutabile, dobbiamo utilizzare l’operatore di dereferenziazione *
per accedere al valore in i
prima di poter utilizzare l’operatore +=
. Parleremo di più dell’operatore di dereferenziazione nella sezione “Seguire il Puntatore al Valore con l’Operatore di Dereferenziazione” del Capitolo 15.
Iterare su un vettore, sia in modo immutabile che mutabile, è sicuro grazie alle regole del borrow checker. Se tentassimo di inserire o rimuovere elementi nei corpi dei cicli for
nell’Elenco 8-7 e nell’Elenco 8-8, otterremmo un errore del compilatore simile a quello che abbiamo ottenuto con il codice nell’Elenco 8-6. Il riferimento al vettore che il ciclo for
detiene impedisce la modifica simultanea dell’intero vettore.
Utilizzo di un Enum per Memorizzare Tipi Multipli
I vettori possono memorizzare solo valori dello stesso tipo. Questo può essere scomodo; ci sono sicuramente casi d’uso per cui è necessario memorizzare una lista di elementi di tipi diversi. Fortunatamente, le varianti di un enum sono definite sotto lo stesso tipo enum, quindi quando abbiamo bisogno che un tipo rappresenti elementi di tipi diversi, possiamo definire e utilizzare un enum!
Ad esempio, diciamo che vogliamo ottenere valori da una riga in un foglio di calcolo in cui alcune delle colonne nella riga contengono 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 tipo: quello dell’enum. Quindi possiamo creare un vettore per contenere quell’enum e quindi, in definitiva, memorizzare tipi diversi. Lo abbiamo dimostrato nell’Elenco 8-9.
rust
enum CellaFoglioCalcolo {
Intero(i32),
VirgolaMobile(f64),
Testo(String),
}
let riga = vec![
CellaFoglioCalcolo::Intero(3),
CellaFoglioCalcolo::Testo(String::from("blu")),
CellaFoglioCalcolo::VirgolaMobile(10.12),
];
Elenco 8-9: Definizione di un enum per memorizzare valori di tipi diversi in un vettore
Rust deve sapere quali tipi saranno nel vettore al momento della compilazione in modo da sapere esattamente quanto spazio di memoria nell’heap sarà necessario per memorizzare ciascun elemento. Dobbiamo anche essere espliciti su quali tipi sono ammessi in questo vettore. Se Rust consentisse a un vettore di contenere qualsiasi tipo, ci sarebbe la possibilità che uno o più dei tipi causassero errori con le operazioni eseguite sugli elementi del vettore. Utilizzando un enum più un’espressione match
significa che Rust garantirà a tempo di compilazione che ogni caso possibile sia gestito, come discusso nel Capitolo 6.
Se non conosci l’insieme esaustivo di tipi che un programma otterrà a tempo di esecuzione per memorizzare in un vettore, la tecnica dell’enum non funzionerà. In tal caso, puoi utilizzare un oggetto trait, di cui parleremo nel Capitolo 17.
Ora che abbiamo discusso alcuni dei modi più comuni per utilizzare i vettori, assicurati di rivedere la documentazione 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.
Rilascio di un Vettore Rilascia i Suoi Elementi
Come qualsiasi altra struttura dati, un vettore viene liberato quando esce dallo scope, come annotato nell’Elenco 8-10.
rust
{
let v = vec![1, 2, 3, 4];
// fai qualcosa con v
} // <- v esce dallo scope e viene liberato qui
Elenco 8-10: Mostra dove viene rilasciato il vettore e i suoi elementi
Quando il vettore viene rilasciato, tutti i suoi contenuti vengono anch’essi rilasciati, il che significa che gli interi che contiene verranno eliminati. Il borrow checker garantisce che qualsiasi riferimento ai contenuti di un vettore venga utilizzato solo mentre il vettore stesso è valido.
Passiamo al prossimo tipo di collezione: String
!