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 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}"),
        _ => (),
    }
}
Listato 6-6: Un 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}");
    }
}
Listato 6-7: Verificare se uno stato esisteva nel 1900 usando condizionali annidati dentro un 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}");
    }
}
Listato 6-8: Usare if let per produrre un valore o ritornare anticipatamente

Questo 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}");
    }
}
Listato 6-9: Usare let...else per semplificare il flusso della funzione

Nota 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.

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.