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() {}
match
che ha come pattern le varianti dell’enumAnalizziamo 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() {}
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); }
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.