Controllare il Flusso con if let e let else
La sintassi if let consente di combinare if e let in un modo meno verboso
per gestire i valori che corrispondono a un singolo pattern, ignorando gli
altri. Considera il programma nel Listato 6-6 che fa matching su un
Option<u8> nella variabile config_max ma vuole eseguire codice solo se il
valore è la variante Some.
fn main() { let config_max = Some(3u8); match config_max { Some(max) => println!("Il massimo è configurato per essere {max}"), _ => (), } }
match che si interessa solo di eseguire codice quando il valore è SomeSe il valore è Some, stampiamo il valore contenuto nella variante Some
legandolo alla variabile max nel pattern. Non vogliamo fare nulla per il
valore None. Per soddisfare l’espressione match dobbiamo aggiungere _ => () dopo aver processato una sola variante, il che, a ben vedere, sembra codice
un po’ inutile.
Invece, possiamo scrivere questo in modo più breve usando if let. Il codice
seguente si comporta allo stesso modo del match nel Listato 6-6:
fn main() { let config_max = Some(3u8); if let Some(max) = config_max { println!("Il massimo è configurato per essere {max}"); } }
La sintassi if let prende un pattern e un’espressione separati da un segno
di uguale. Funziona come un match, dove l’espressione è data al match e il
pattern è il suo primo ramo. In questo caso il pattern è Some(max), e
max si lega al valore dentro il Some. Possiamo quindi usare max nel corpo
del blocco if let nello stesso modo in cui lo usavamo nel corrispondente ramo
del match. Il codice nel blocco if let viene eseguito solo se il valore
corrisponde al pattern.
Usare if let significa meno digitazione, meno indentazione e meno codice poco
utile. Tuttavia si perde il controllo di esaustività che il match impone e che
garantisce di non dimenticare di gestire dei casi. La scelta tra match e if let dipende da cosa stai facendo in quella situazione particolare e se un
codice più conciso valga la perdita del controllo esaustivo.
In altre parole, puoi pensare a if let come ad una espressione match ridotta
all’osso che esegue codice quando il valore corrisponde a un pattern e poi
ignora tutti gli altri valori.
Possiamo includere un else con un if let. Il blocco di codice che accompagna
l’else è lo stesso blocco che andrebbe con il caso _ nell’espressione
match equivalente all’if let con else. Ricorda la definizione dell’enum
Moneta nel Listato 6-4, dove la variante Quarter conteneva anche un valore
StatoUSA. Se volessimo contare tutte le monete non-quarter che vediamo
annunciando anche lo stato dei quarter, potremmo farlo con un’espressione
match, così:
#[derive(Debug)] enum StatoUSA { Alabama, Alaska, // --taglio-- } enum Moneta { Penny, Nickel, Dime, Quarter(StatoUSA), } fn main() { let moneta = Moneta::Penny; let mut conteggio = 0; match moneta { Moneta::Quarter(stato) => println!("Quarter statale del {stato:?}!"), _ => conteggio += 1, } }
Oppure potremmo usare un if let e un else, così:
#[derive(Debug)] enum StatoUSA { Alabama, Alaska, // --taglio-- } enum Moneta { Penny, Nickel, Dime, Quarter(StatoUSA), } fn main() { let moneta = Moneta::Penny; let mut conteggio = 0; if let Moneta::Quarter(stato) = moneta { println!("Quarter statale del {stato:?}!"); } else { conteggio += 1; } }
Restare sul “Percorso Felice” con let...else
Una buona pratica è eseguire una computazione quando un valore è presente e
restituire un valore di default altrimenti. Continuando con il nostro esempio
delle monete con un valore StatoUSA, se volessimo dire qualcosa di divertente
a seconda di quanto fosse vecchio lo stato sul quarter, potremmo introdurre un
metodo su StatoUSA per verificare l’età dello stato, così:
#[derive(Debug)] // so we can inspect the state in a minute enum StatoUSA { Alabama, Alaska, // --taglio-- } impl StatoUSA { fn esistente_nel(&self, anno: u16) -> bool { match self { StatoUSA::Alabama => anno >= 1819, StatoUSA::Alaska => anno >= 1959, // --taglio-- } } } enum Moneta { Penny, Nickel, Dime, Quarter(StatoUSA), } fn desc_quarter_statale(moneta: Moneta) -> Option<String> { if let Moneta::Quarter(stato) = moneta { if stato.esistente_nel(1900) { Some(format!("{stato:?} è abbastanza vecchio, per l'America!")) } else { Some(format!("{stato:?} è abbastanza recente.")) } } else { None } } fn main() { if let Some(desc) = desc_quarter_statale(Moneta::Quarter(StatoUSA::Alaska)) { println!("{desc}"); } }
Poi potremmo usare if let per fare matching sul tipo di moneta, introducendo
una variabile stato all’interno del corpo della condizione, come nel Listato
6-7.
#[derive(Debug)] // so we can inspect the state in a minute enum StatoUSA { Alabama, Alaska, // --taglio-- } impl StatoUSA { fn esistente_nel(&self, anno: u16) -> bool { match self { StatoUSA::Alabama => anno >= 1819, StatoUSA::Alaska => anno >= 1959, // --taglio-- } } } enum Moneta { Penny, Nickel, Dime, Quarter(StatoUSA), } fn desc_quarter_statale(moneta: Moneta) -> Option<String> { if let Moneta::Quarter(stato) = moneta { if stato.esistente_nel(1900) { Some(format!("{stato:?} è abbastanza vecchio, per l'America!")) } else { Some(format!("{stato:?} è abbastanza recente.")) } } else { None } } fn main() { if let Some(desc) = desc_quarter_statale(Moneta::Quarter(StatoUSA::Alaska)) { println!("{desc}"); } }
if letQuesto risolve il problema, ma ha spostato il lavoro nel corpo dell’if let, e
se il lavoro da fare è più complicato potrebbe risultare difficile seguire come
i rami di alto livello si relazionano. Potremmo anche sfruttare il fatto che le
espressioni producono un valore, o per produrre stato dall’if let o per
ritornare anticipatamente, come in Listato 6-8. (Si potrebbe fare qualcosa di
simile anche con un match.)
#[derive(Debug)] // so we can inspect the state in a minute enum StatoUSA { Alabama, Alaska, // --taglio-- } impl StatoUSA { fn esistente_nel(&self, anno: u16) -> bool { match self { StatoUSA::Alabama => anno >= 1819, StatoUSA::Alaska => anno >= 1959, // --taglio-- } } } enum Moneta { Penny, Nickel, Dime, Quarter(StatoUSA), } fn desc_quarter_statale(moneta: Moneta) -> Option<String> { let stato = if let Moneta::Quarter(stato) = moneta { stato } else { return None; }; if stato.esistente_nel(1900) { Some(format!("{stato:?} è abbastanza vecchio, per l'America!")) } else { Some(format!("{stato:?} è abbastanza recente.")) } } fn main() { if let Some(desc) = desc_quarter_statale(Moneta::Quarter(StatoUSA::Alaska)) { println!("{desc}"); } }
if let per produrre un valore o ritornare anticipatamenteQuesto però è un po’ scomodo da seguire: un ramo dell’if let produce un valore
e l’altro ritorna dalla funzione completamente.
Per rendere più esprimibile questo pattern comune, Rust ha let...else. La
sintassi let...else prende un pattern a sinistra e un’espressione a destra,
molto simile a if let, ma non ha un ramo if, soltanto un ramo else. Se il
pattern corrisponde, legherà il valore estratto dal pattern nello scope
esterno. Se il pattern non corrisponde, il flusso va nel ramo else, che deve
restituire dalla funzione.
Nel Listato 6-9 puoi vedere come Listato 6-8 appare usando let...else al posto
di if let.
#[derive(Debug)] // so we can inspect the state in a minute enum StatoUSA { Alabama, Alaska, // --taglio-- } impl StatoUSA { fn esistente_nel(&self, anno: u16) -> bool { match self { StatoUSA::Alabama => anno >= 1819, StatoUSA::Alaska => anno >= 1959, // --taglio-- } } } enum Moneta { Penny, Nickel, Dime, Quarter(StatoUSA), } fn desc_quarter_statale(moneta: Moneta) -> Option<String> { let Moneta::Quarter(stato) = moneta else { return None; }; if stato.esistente_nel(1900) { Some(format!("{stato:?} è abbastanza vecchio, per l'America!")) } else { Some(format!("{stato:?} è abbastanza recente.")) } } fn main() { if let Some(desc) = desc_quarter_statale(Moneta::Quarter(StatoUSA::Alaska)) { println!("{desc}"); } }
let...else per semplificare il flusso della funzioneNota che in questo modo si resta sul “percorso felice” nel corpo principale
della funzione, senza avere un controllo di flusso significativamente diverso
per i due rami come invece succedeva con if let.
Se hai una situazione in cui la logica è troppo verbosa per essere espressa con
un match, ricorda che anche if let e let...else sono nella tua cassetta
degli attrezzi di Rust.
Riepilogo
Abbiamo ora coperto come usare le enum per creare type personalizzati che
possono essere uno tra un insieme di valori enumerati. Abbiamo mostrato come il
type Option<T> della libreria standard ti aiuta a usare il sistema dei
type per prevenire errori. Quando i valori delle enum contengono dati, puoi
usare match o if let per estrarre e usare quei valori, a seconda di quanti
casi devi gestire.
I tuoi programmi Rust possono ora esprimere concetti nel tuo dominio usando
struct ed enum. Creare type personalizzati da usare nella tua API assicura
maggiore sicurezza dei dati (type safety): il compilatore garantirà che le tue
funzioni ricevano solo i valori del type che ciascuna funzione si aspetta.
Per fornire un’API ben organizzata ai tuoi utenti, chiara da usare ed esponendo solo ciò che serve, passiamo ora ai moduli di Rust.