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

Controllare il Flusso col Costrutto match

Rust offre un costrutto di controllo del flusso estremamente potente chiamato match (corrisponde, combacia) che permette di confrontare un valore con una serie di pattern ed eseguire codice in base al pattern che corrisponde. I pattern possono essere composti da valori letterali, nomi di variabili, caratteri jolly e molte altre cose; il Capitolo 19 copre tutte le diverse tipologie di pattern e cosa fanno. La potenza di match deriva dall’espressività dei pattern e dal fatto che il compilatore conferma che tutti i casi possibili sono gestiti.

Pensa a un’espressione match come a una macchina che smista monete: le monete scivolano lungo una guida con fori di varie dimensioni e ciascuna moneta cade nel primo foro in cui entra. Allo stesso modo, i valori passano attraverso ogni pattern in un match, e al primo pattern in cui il valore «entra», il valore viene fatto ricadere nel blocco di codice associato per essere usato durante l’esecuzione.

Parlando di monete, usiamole come esempio con match! Possiamo scrivere una funzione che prende una moneta USA sconosciuta e, in modo simile alla macchina conta-monete, determina quale moneta sia e restituisce il suo valore in centesimi, come mostrato nel Listato 6-3.

enum Moneta {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn valore_in_cent(moneta: Moneta) -> u8 {
    match moneta {
        Moneta::Penny => 1,
        Moneta::Nickel => 5,
        Moneta::Dime => 10,
        Moneta::Quarter => 25,
    }
}

fn main() {}
Listato 6-3: Un’enum e un’espressione match che ha come pattern le varianti dell’enum

Analizziamo il match nella funzione valore_in_cent. Prima troviamo la parola chiave match seguita da un’espressione, che in questo caso è il valore moneta. Questo sembra molto simile a un’espressione condizionale usata con if, ma c’è una grande differenza: con if la condizione deve valutarsi a un valore Booleano, mentre qui può essere di qualsiasi type. Il type di moneta in questo esempio è l’enum Moneta che abbiamo definito nella prima riga.

Seguono i rami di match. Un ramo è composto da due parti: un pattern e del codice. Il primo ramo qui ha come pattern il valore Moneta::Penny e poi l’operatore => che separa il pattern dal codice da eseguire. Il codice in questo caso è semplicemente il valore 1. Ogni ramo è separato dal successivo da una virgola.

Quando l’espressione match viene eseguita, confronta il valore risultante con il pattern di ogni ramo, in ordine. Se un pattern corrisponde al valore, viene eseguito il codice associato a quel pattern. Se quel pattern non corrisponde, l’esecuzione continua con il ramo successivo, proprio come nella macchina che smista monete. Possiamo avere tanti rami quanti ce ne servono: nel Listato 6-3, il nostro match ha quattro rami.

Il codice associato a ciascun ramo è un’espressione, e il valore risultante dell’espressione nel ramo che corrisponde è il valore restituito per l’intera espressione match.

Di solito non usiamo le parentesi graffe se il codice del ramo è breve, come nel Listato 6-3 dove ogni ramo restituisce solo un valore. Se vuoi eseguire più righe di codice in un ramo del match, devi usare le parentesi graffe, e la virgola che segue il ramo diventa opzionale. Per esempio, il codice seguente stampa “Penny fortunato!” ogni volta che il metodo viene chiamato con una Coin::Penny, ma restituisce comunque l’ultimo valore del blocco, 1:

enum Moneta {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn valore_in_cent(moneta: Moneta) -> u8 {
    match moneta {
        Moneta::Penny => {
            println!("Penny fortunato!");
            1
        }
        Moneta::Nickel => 5,
        Moneta::Dime => 10,
        Moneta::Quarter => 25,
    }
}

fn main() {}

Pattern che Si Legano ai Valori

Un’altra caratteristica utile dei rami del match è che possono legarsi alle parti dei valori che corrispondono al pattern. È così che possiamo estrarre valori dalle varianti delle enum.

Per esempio, modifichiamo una delle nostre varianti dell’enum per contenerci dei dati. Dal 1999 al 2008, gli Stati Uniti coniarono quarter con design diversi per ciascuno dei 50 stati su un lato. Nessun’altra moneta aveva design statali, quindi solo i quarter hanno questa caratteristica peculiare. Possiamo aggiungere questa informazione al nostra enum cambiando la variante Quarter per includere un valore StatoUSA all’interno, come fatto nel Listato 6-4.

#[derive(Debug)] // così possiamo vederne i valori tra un po'
enum StatoUSA {
    Alabama,
    Alaska,
    // --taglio--
}

enum Moneta {
    Penny,
    Nickel,
    Dime,
    Quarter(StatoUSA),
}

fn main() {}
Listato 6-4: Un’enum Moneta in cui la variante Quarter contiene anche un valore StatoUSA

Immaginiamo che un amico stia cercando di collezionare tutti e 50 i quarter statali. Mentre separiamo il nostro resto per tipo di moneta, guarderemo anche il nome dello stato associato a ciascun quarter così, se è uno che al nostro amico manca, può aggiungerlo alla collezione.

Nell’espressione match per questo codice, aggiungiamo una variabile chiamata stato al pattern che corrisponde ai valori della variante Coin::Quarter. Quando un Coin::Quarter corrisponde, la variabile stato si legherà al valore dello stato di quel quarter. Possiamo poi usare stato nel codice di quel ramo, così:

#[derive(Debug)]
enum StatoUSA {
    Alabama,
    Alaska,
    // --taglio--
}

enum Moneta {
    Penny,
    Nickel,
    Dime,
    Quarter(StatoUSA),
}

fn valore_in_cent(moneta: Moneta) -> u8 {
    match moneta {
        Moneta::Penny => 1,
        Moneta::Nickel => 5,
        Moneta::Dime => 10,
        Moneta::Quarter(stato) => {
            println!("Quarter statale del {stato:?}!");
            25
        }
    }
}

fn main() {
    valore_in_cent(Moneta::Quarter(StatoUSA::Alaska));
}

Se chiamassimo valore_in_cent(Moneta::Quarter(StatoUSA::Alaska)), moneta sarebbe Moneta::Quarter(StatoUSA::Alaska). Quando confrontiamo quel valore con ciascuno dei rami del match, nessuna corrisponde fino a che non raggiungiamo Moneta::Quarter(stato). A quel punto stato sarà vincolato al valore StatoUSA::Alaska. Possiamo quindi usare quel vincolo nell’espressione println!, ottenendo così il valore interno dello stato dalla variante Moneta::Quarter.

Corrispondenza con Option<T>

Nella sezione precedente volevamo ottenere il valore interno T di Some quando si usa Option<T>; possiamo anche gestire Option<T> usando match, proprio come abbiamo fatto con l’enum Moneta! Invece di confrontare monete, confronteremo le varianti di Option<T>, ma il funzionamento dell’espressione match rimane lo stesso.

Supponiamo di voler scrivere una funzione che prende un Option<i32> e, se c’è un valore dentro, aggiunge 1 a quel valore. Se non c’è un valore dentro, la funzione dovrebbe restituire il valore None e non tentare di eseguire alcuna operazione.

Questa funzione è molto semplice da scrivere, grazie a match, e apparirà come nel Listato 6-5.

fn main() {
    fn più_uno(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let cinque = Some(5);
    let sei = più_uno(cinque);
    let nulla = più_uno(None);
}
Listato 6-5: Una funzione che utilizza un’espressione match su una Option<i32>

Esaminiamo la prima esecuzione di più_uno in maggiore dettaglio. Quando chiamiamo più_uno(cinque), la variabile x nel corpo di più_uno avrà il valore Some(5). Quindi confrontiamo quello rispetto a ciascun ramo del match:

fn main() {
    fn più_uno(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let cinque = Some(5);
    let sei = più_uno(cinque);
    let nulla = più_uno(None);
}

Il valore Some(5) non corrisponde al pattern None, quindi si continua con il ramo successivo:

fn main() {
    fn più_uno(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let cinque = Some(5);
    let sei = più_uno(cinque);
    let nulla = più_uno(None);
}

Some(5) corrisponde a Some(i)? Sì! Abbiamo la stessa variante. i si lega al valore contenuto in Some, quindi i assume il valore 5. Il codice nel ramo del match viene quindi eseguito: aggiungiamo 1 al valore di i e creiamo un nuovo valore Some con il totale 6 all’interno.

Consideriamo ora la seconda chiamata di più_uno nel Listato 6-5, dove x è None. Entriamo nel match e confrontiamolo con il primo ramo:

fn main() {
    fn più_uno(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let cinque = Some(5);
    let sei = più_uno(cinque);
    let nulla = più_uno(None);
}

Corrisponde! Non c’è alcun valore a cui aggiungere, quindi il programma si ferma e restituisce il valore None sul lato destro di =>. Poiché il primo ramo ha corrisposto, nessun altro ramo viene confrontato.

Combinare match ed enum è utile in molte situazioni. Vedrai questo schema spesso nel codice Rust: fai match su un’enum, leghi una variabile ai dati interni e poi esegui codice basato su di essi. All’inizio è un po’ ostico, ma una volta che ci prendi la mano vorrai averlo in tutti i linguaggi. È un costrutto tra i preferiti dagli utenti.

Le Corrispondenze sono Esaustive

C’è un altro aspetto di match da discutere: i pattern dei rami devono coprire tutte le possibilità. Considera questa versione della nostra funzione più_uno, che contiene un bug e non si compilerà:

fn main() {
    fn più_uno(x: Option<i32>) -> Option<i32> {
        match x {
            Some(i) => Some(i + 1),
        }
    }

    let cinque = Some(5);
    let sei = più_uno(cinque);
    let nulla = più_uno(None);
}

Non abbiamo gestito il caso None, quindi questo codice provocherà un errore. Per fortuna, è un errore che Rust sa come intercettare. Se proviamo a compilare questo codice, otterremo questo errore:

$ cargo run
   Compiling enums v0.1.0 (file:///progetti/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
 --> src/main.rs:4:15
  |
4 |         match x {
  |               ^ pattern `None` not covered
  |
note: `Option<i32>` defined here
 --> /rustc/8e62bfd311791bfd9dca886abdfbab07ec54d8b4/library/core/src/option.rs:593:1
 ::: /rustc/8e62bfd311791bfd9dca886abdfbab07ec54d8b4/library/core/src/option.rs:597:5
  |
  = note: not covered
  = note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown
  |
5 ~             Some(i) => Some(i + 1),
6 ~             None => todo!(),
  |

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

Rust sa che non abbiamo coperto ogni possibile caso, e sa persino quale pattern abbiamo dimenticato! I match in Rust sono esaustivi (exhaustive): dobbiamo coprire ogni possibilità affinché il codice sia valido. Soprattutto nel caso di Option<T>, quando Rust ci impedisce di dimenticare di gestire esplicitamente il caso None, ci protegge dall’assumere che abbiamo un valore quando potremmo avere null, rendendo impossibile “l’errore da un miliardo di dollari” accennato nel capitolo precedente.

Pattern Pigliatutto e Segnaposto _

Usando le enum, possiamo anche eseguire determinate azioni per alcuni valori particolari, ma per tutti gli altri valori adottare un’azione predefinita. Immagina di implementare un gioco dove, se tiri un 3 in un lancio di dadi, il tuo giocatore non si muove ma riceve un nuovo cappello buffo. Se tiri un 7, il giocatore perde il cappello buffo. Per tutti gli altri valori, il giocatore si muove di quel numero di spazi sulla tavola di gioco. Ecco un match che implementa quella logica, con il risultato del lancio specificato anziché casuale, e tutta l’altra logica rappresentata da funzioni senza corpo perché implementarli esula da questo esempio:

fn main() {
    let tiro_dadi = 9;
    match tiro_dadi {
        3 => metti_cappello_buffo(),
        7 => togli_cappello_buffo(),
        altro => muovi_giocatore(altro),
    }

    fn metti_cappello_buffo() {}
    fn togli_cappello_buffo() {}
    fn muovi_giocatore(num_spazi: u8) {}
}

Per i primi due rami, i pattern sono i valori letterali 3 e 7. Per l’ultimo ramo che copre tutti gli altri valori possibili, il pattern è la variabile che abbiamo scelto di chiamare altro. Il codice che viene eseguito per il ramo altro usa la variabile passando il suo valore alla funzione muovi_giocatore.

Questo codice compila, anche se non abbiamo elencato tutti i possibili valori che un u8 può avere, perché l’ultimo pattern corrisponderà a tutti i valori non specificamente elencati. Questo pattern pigliatutto (catch-all) soddisfa il requisito che match deve essere esaustivo. Nota che dobbiamo mettere il ramo pigliatutto per ultimo perché i pattern sono valutati in ordine. Se mettessimo il ramo pigliatutto prima, gli altri rami non verrebbero mai eseguiti, quindi Rust ci avvertirebbe se aggiungessimo rami dopo un pigliatutto!

Rust ha anche un pattern che possiamo usare quando vogliamo un pigliatutto ma non vogliamo usare il valore corrispondente: _ è un pattern speciale che corrisponde a qualsiasi valore e non si lega a quel valore. Questo dice a Rust che non useremo il valore, quindi Rust non ci segnalerà una variabile inutilizzata.

Cambiamo le regole del gioco: ora, se tiri qualsiasi cosa diversa da 3 o 7, devi rilanciare. Non abbiamo più bisogno di usare il valore pigliatutto, quindi possiamo cambiare il codice per usare _ al posto della variabile chiamata altro:

fn main() {
    let tiro_dadi = 9;
    match tiro_dadi {
        3 => metti_cappello_buffo(),
        7 => togli_cappello_buffo(),
        _ => tira_ancora(),
    }

    fn metti_cappello_buffo() {}
    fn togli_cappello_buffo() {}
    fn tira_ancora() {}
}

Anche questo esempio soddisfa il requisito di esaustività perché stiamo esplicitamente ignorando tutti gli altri valori nell’ultimo ramo; non abbiamo dimenticato nulla.

Infine, cambiamo ancora una volta le regole del gioco in modo che non succeda nient’altro nel tuo turno se tiri qualcosa di diverso da 3 o 7. Possiamo esprimerlo usando il valore unit (la tupla vuota) come codice associato al ramo _:

fn main() {
    let tiro_dadi = 9;
    match tiro_dadi {
        3 => metti_cappello_buffo(),
        7 => togli_cappello_buffo(),
        _ => (),
    }

    fn metti_cappello_buffo() {}
    fn togli_cappello_buffo() {}
}

Qui stiamo dicendo esplicitamente a Rust che non useremo alcun altro valore che non corrisponda a un pattern in un ramo precedente, e non vogliamo eseguire alcun codice in questo caso.

C’è molto altro sui pattern e sul matching che tratteremo nel Capitolo 19. Per ora, passiamo alla sintassi if let, che può essere utile nelle situazioni più semplici in cui l’espressione match risulta un po’ verbosa.