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 è Some
Se 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 let
Questo 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.