Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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();
}
Listato 8-1: Creazione di un nuovo vettore vuoto per contenere valori di type 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];
}
Listato 8-2: Creazione di un nuovo vettore contenente valori

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);
}
Listato 8-3: Utilizzo del metodo push per aggiungere valori a un vettore

Come 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."),
    }
}
Listato 8-4: Utilizzo della sintassi di indicizzazione e utilizzo del metodo get per accedere a un elemento in un vettore

Sono 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);
}
Listato 8-5: Tentativo di accesso all’elemento all’indice 100 in un vettore contenente cinque elementi

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}");
}
Listato 8-6: Tentativo di aggiungere un elemento a un vettore in compresenza di un reference all’oggetto

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}");
    }
}
Listato 8-7: Stampa di ogni elemento in un vettore iterando sugli elementi utilizzando un ciclo 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;
    }
}
Listato 8-8: Iterare su reference mutabili a elementi in un vettore

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),
    ];
}
Listato 8-9: Definizione di un enum per memorizzare valori di type diversi in un vettore

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
}
Listato 8-10: Mostra dove vengono rilasciati il vettore e i suoi elementi

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!