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

Tutti i Posti in cui Possono Essere Utilizzati i Pattern

I pattern compaiono in diversi punti di Rust e li hai usati senza rendertene conto! Questa sezione illustra tutti i posti in cui i pattern sono validi.

Rami match

Come discusso nel Capitolo 6, utilizziamo i pattern nei rami delle espressioni match. Formalmente, le espressioni match sono definite come la parola chiave match, un valore su cui effettuare la corrispondenza e uno o più rami di corrispondenza costituiti da un pattern e un’espressione da eseguire se il valore corrisponde al pattern di quel ramo, in questo modo:

match VALUE {
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
}

Ad esempio, ecco l’espressione match del Listato 6-5 che corrisponde a un valore Option<i32> nella variabile x:

match x {
None => None,
Some(i) => Some(i + 1),
}

I pattern in questa espressione match sono None e Some(i) a sinistra di ciascuna freccia.

Un requisito per le espressioni match è che siano esaustive, nel senso che tutte le possibilità per il valore nell’espressione match devono essere considerate. Un modo per assicurarsi di aver coperto ogni possibilità è avere un pattern generico per l’ultimo caso: ad esempio, un nome di variabile che corrisponde a qualsiasi valore non può mai fallire e quindi copre tutti i casi rimanenti.

Il pattern specifico _ corrisponderà a qualsiasi cosa, ma non si vincola mai a una variabile, quindi viene spesso utilizzato nell’ultimo ramo di corrispondenza. Il pattern _ può essere utile quando si desidera ignorare qualsiasi valore non specificato, ad esempio. Tratteremo il pattern _ più dettagliatamente in “Ignorare Valori In un Pattern più avanti in questo capitolo.

Istruzioni let

Prima di questo capitolo, avevamo discusso esplicitamente dell’uso dei pattern solo con match e if let, ma in realtà abbiamo utilizzato pattern anche in altri contesti, anche nelle istruzioni let. Ad esempio, si consideri questa semplice assegnazione di variabile con let:

#![allow(unused)]
fn main() {
let x = 5;
}

Ogni volta che avete utilizzato un’istruzione let come questa, avete utilizzato dei pattern, anche se potreste non esservene accorti! Più formalmente, un’istruzione let si presenta così:

let PATTERN = EXPRESSION;

In istruzioni come let x = 5; con un nome di variabile nello slot PATTERN, il nome della variabile è solo una forma particolarmente semplice di pattern. Rust confronta l’espressione con il pattern e assegna qualsiasi nome trovi. Quindi, nell’esempio let x = 5;, x è un pattern che significa “associa ciò che corrisponde qui alla variabile x”. Poiché il nome x è l’intero pattern, questo pattern significa effettivamente “associa tutto alla variabile x, qualunque sia il valore”.

Per comprendere più chiaramente l’aspetto di pattern-matching di let, si consideri il Listato 19-1, che utilizza un pattern con let per destrutturare una tupla.

fn main() {
    let (x, y, z) = (1, 2, 3);
}
Listato 19-1: Utilizzo di un pattern per destrutturare una tupla e creare tre variabili contemporaneamente

Qui, confrontiamo una tupla con un pattern. Rust confronta il valore (1, 2, 3) con il pattern (x, y, z) e verifica che il valore corrisponde al pattern, in quanto vede che il numero di elementi è lo stesso in entrambi, quindi Rust associa 1 a x, 2 a y e 3 a z. Si può pensare a questo pattern di tupla come all’annidamento di tre pattern di variabili individuali al suo interno.

Se il numero di elementi nel pattern non corrisponde al numero di elementi nella tupla, il tipo complessivo non corrisponderà e si verificherà un errore del compilatore. Ad esempio, il Listato 19-2 mostra un tentativo di destrutturare una tupla con tre elementi in due variabili, che non funzionerà.

fn main() {
    let (x, y) = (1, 2, 3);
}
Listato 19-2: Costruzione errata di un pattern le cui variabili non corrispondono al numero di elementi nella tupla

Il tentativo di compilare questo codice genera questo tipo di errore:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0308]: mismatched types
 --> src/main.rs:2:9
  |
2 |     let (x, y) = (1, 2, 3);
  |         ^^^^^^   --------- this expression has type `({integer}, {integer}, {integer})`
  |         |
  |         expected a tuple with 3 elements, found one with 2 elements
  |
  = note: expected tuple `({integer}, {integer}, {integer})`
             found tuple `(_, _)`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `patterns` (bin "patterns") due to 1 previous error

Per correggere l’errore, potremmo ignorare uno o più valori nella tupla usando _ o .., come vedrai nella sezione “Ignorare Valori In un Pattern. Se il problema è che abbiamo troppe variabili nel pattern, la soluzione è far corrispondere i tipi rimuovendo le variabili in modo che il numero di variabili sia uguale al numero di elementi nella tupla.

Espressioni Condizionali if let

Nel Capitolo 6, abbiamo discusso come utilizzare le espressioni if let principalmente come un modo più breve per scrivere l’equivalente di un match che corrisponde a un solo caso. Facoltativamente, if let può avere un else corrispondente contenente il codice da eseguire se il pattern in if let non corrisponde.

Il Listato 19-3 mostra che è anche possibile combinare e abbinare le espressioni if let, else if e else if let. Ciò offre maggiore flessibilità rispetto a un’espressione match in cui possiamo esprimere un solo valore da confrontare con i pattern. Inoltre, Rust non richiede che le condizioni in una serie di rami if let, else if e else if let siano correlate tra loro.

Il codice nel Listato 19-3 determina il colore da utilizzare per lo sfondo in base a una serie di controlli per diverse condizioni. Per questo esempio, abbiamo creato variabili con valori hardcoded che un programma reale potrebbe ricevere dall’input dell’utente.

File: src/main.rs
fn main() {
    let colore_preferito: Option<&str> = None;
    let e_martedi = false;
    let eta: Result<u8, _> = "34".parse();

    if let Some(color) = colore_preferito {
        println!("Usando il tuo colore preferito, {color}, come sfondo");
    } else if e_martedi {
        println!("Martedì è il giorno verde!");
    } else if let Ok(eta) = eta {
        if eta > 30 {
            println!("Usando il viola come colore di sfondo");
        } else {
            println!("Usando l'arancione come colore di sfondo");
        }
    } else {
        println!("Usando il blu come colore di sfondo");
    }
}
Listato 19-3: Combinazione di if let, else if, else if let e else

Se l’utente specifica un colore preferito, quel colore viene utilizzato come sfondo. Se non viene specificato alcun colore preferito e oggi è martedì, il colore di sfondo è verde. Altrimenti, se l’utente specifica la propria età come stringa e possiamo analizzarla come un numero correttamente, il colore sarà viola o arancione a seconda del valore del numero. Se nessuna di queste condizioni si applica, il colore di sfondo è blu.

Questa struttura condizionale ci consente di supportare requisiti complessi. Con i valori hardcoded che abbiamo qui, questo esempio stamperà Usando il viola come colore di sfondo.

Si può notare che if let può anche introdurre nuove variabili che oscurano le variabili esistenti allo stesso modo in cui match può farlo: la riga if let Ok(eta) = eta introduce una nuova variabile eta che contiene il valore all’interno della variante Ok, oscurando la variabile eta esistente. Ciò significa che dobbiamo inserire la condizione if eta > 30 all’interno di quel blocco: non possiamo combinare queste due condizioni in if let Ok(eta) = eta && eta > 30. Il nuovo eta che vogliamo confrontare con 30 non è valido finché il nuovo ambito non inizia con la parentesi graffa.

Lo svantaggio dell’utilizzo di espressioni if let è che il compilatore non verifica l’esaustività, mentre con le espressioni match lo fa. Se omettessimo l’ ultimo blocco else e quindi non gestissimo alcuni casi, il compilatore non ci avviserebbe del possibile bug logico.

Cicli Condizionali while let

Simile nella costruzione a if let, il ciclo condizionale while let consente a un ciclo while di essere eseguito finché un pattern continua a corrispondere. Nel Listato 19-4 mostriamo un ciclo while let che attende i messaggi inviati tra thread, ma in questo caso controlla un Result invece di un Option.

fn main() {
    let (tx, rx) = std::sync::mpsc::channel();
    std::thread::spawn(move || {
        for val in [1, 2, 3] {
            tx.send(val).unwrap();
        }
    });

    while let Ok(value) = rx.recv() {
        println!("{value}");
    }
}
Listato 19-4: Utilizzo di un ciclo while let per stampare valori finché rx.recv() restituisce Ok

Questo esempio stampa 1, 2 e poi 3. Il metodo recv prende il primo messaggio dal lato ricevente del canale e restituisce Ok(value). Quando abbiamo visto per la prima volta recv nel Capitolo 16, abbiamo analizzato direttamente l’errore, o abbiamo interagito con esso come un iteratore usando un ciclo for. Come mostra il Listato 19-4, tuttavia, possiamo anche usare while let, perché il metodo recv restituisce Ok ogni volta che arriva un messaggio, finché il mittente esiste, e poi produce un Err una volta che il mittente si disconnette.

Cicli for

In un ciclo for, il valore che segue direttamente la parola chiave for è un pattern. Ad esempio, in for x in y, x è il pattern. Il Listato 19-5 mostra come utilizzare un pattern in un ciclo for per destrutturare, o scomporre una tupla come parte del ciclo for.

fn main() {
    let v = vec!['a', 'b', 'c'];

    for (indice, valore) in v.iter().enumerate() {
        println!("{valore} è all'indice {indice}");
    }
}
Listato 19-5: Utilizzo di un pattern in un ciclo for per destrutturare una tupla

Il codice nel Listato 19-5 stamperà quanto segue:

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.52s
     Running `target/debug/patterns`
a is at index 0
b is at index 1
c is at index 2

Adattiamo un iteratore utilizzando il metodo enumerate in modo che produca un valore e l’indice per quel valore, inserito in una tupla. Il primo valore prodotto è la tupla (0, 'a'). Quando questo valore viene confrontato con il pattern (indice, valore), indice sarà 0 e valore sarà a, stampando la prima riga dell’ output.

Parametri di Funzione

Anche i parametri di funzione possono essere pattern. Il codice nel Listato 19-6, che dichiara una funzione chiamata foo che accetta un parametro chiamato x di tipo i32, dovrebbe ormai risultare familiare.

fn foo(x: i32) {
    // code goes here
}

fn main() {}
Listato 19-6: Una firma di funzione usa pattern nei parametri

La parte x è un pattern! Come abbiamo fatto con let, potremmo abbinare una tupla negli argomenti di una funzione al pattern. Il Listato 19-7 suddivide i valori in una tupla mentre la passiamo a una funzione.

File: src/main.rs
fn stampa_coordinate(&(x, y): &(i32, i32)) {
    println!("Posizione corrente: ({x}, {y})");
}

fn main() {
    let punto = (3, 5);
    stampa_coordinate(&punto);
}
Listato 19-7: Una funzione con parametri che destrutturano una tupla

Questo codice stampa Posizione corrente: (3, 5). I valori &(3, 5) corrispondono al pattern &(x, y), quindi x è il valore 3 e y è il valore 5.

Possiamo anche usare i pattern nelle liste di parametri di chiusura allo stesso modo delle liste di parametri di funzione, perché le chiusure sono simili alle funzioni, come discusso nel Capitolo 13.

A questo punto, avete visto diversi modi per usare i pattern, ma i pattern non funzionano allo stesso modo in tutti i casi in cui possiamo usarli. In alcuni casi, i pattern devono essere inconfutabili; in altre circostanze, possono essere confutabili. Discuteremo questi due concetti più avanti.