Shopping cart

0

Cart

  • 0 item

Nessun prodotto nel carrello.

All categories
 Corso Gratuito di Programmazione Rust Lezione 028 – Memorizzare elenchi di valori con i vettori

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!

Leave a Reply

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *