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

Un contrôle de flux concis avec if let et let...else

La syntaxe if let vous permet de combiner if et let de manière moins verbeuse pour gérer les valeurs qui correspondent à un motif tout en ignorant les autres. Considérez le programme de l’encart 6-6 qui fait un filtrage sur une valeur Option<u8> dans la variable config_max mais ne veut exécuter du code que si la valeur est la variante Some.

fn main() {
    let config_max = Some(3u8);
    match config_max {
        Some(max) => println!("The maximum is configured to be {max}"),
        _ => (),
    }
}
Listing 6-6: A match that only cares about executing code when the value is Some

Si la valeur est Some, nous affichons la valeur de la variante Some en liant la valeur à la variable max dans le motif. Nous ne voulons rien faire avec la valeur None. Pour satisfaire l’expression match, nous devons ajouter _ => () après avoir traité une seule variante, ce qui est du code passe-partout ennuyeux à ajouter.

À la place, nous pourrions écrire cela de manière plus courte en utilisant if let. Le code suivant se comporte de la même manière que le match de l’encart 6-6 : rust {{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-12-if-let/src/main.rs:here}}

fn main() {
    let config_max = Some(3u8);
    if let Some(max) = config_max {
        println!("The maximum is configured to be {max}");
    }
}

La syntaxe if let prend un motif et une expression séparés par un signé égal. Elle fonctionne de la même manière qu’un match, où l’expression est donnée au match et le motif est son premier bras. Dans ce cas, le motif est Some(max), et max se lie à la valeur à l’intérieur du Some. Nous pouvons ensuite utiliser max dans le corps du bloc if let de la même manière que nous avons utilisé max dans le bras match correspondant. Le code dans le bloc if let ne s’exécute que si la valeur correspond au motif.

Utiliser if let signifie moins de saisie, moins d’indentation et moins de code passe-partout. Cependant, vous perdez la vérification exhaustive que match impose et qui garantit que vous n’oubliez pas de gérer certains cas. Le choix entre match et if let dépend de ce que vous faites dans votre situation particulière et de savoir si gagner en concision est un compromis approprié par rapport à la perte de la vérification exhaustive.

En d’autres termes, vous pouvez considérer if let comme du sucre syntaxique pour un match qui exécute du code quand la valeur correspond à un motif puis ignore toutes les autres valeurs.

Nous pouvons inclure un else avec un if let. Le bloc de code qui accompagne le else est le même que le bloc de code qui accompagnerait le cas _ dans l’expression match équivalente au if let et else. Rappelons la définition de l’enum Coin dans l’encart 6-4, où la variante Quarter contenait aussi une valeur UsState. Si nous voulions compter toutes les pièces qui ne sont pas des quarters tout en annonçant l’État des quarters, nous pourrions le faire avec une expression match, comme ceci : rust {{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-13-count-and-announce-match/src/main.rs:here}}

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {
    let coin = Coin::Penny;
    let mut count = 0;
    match coin {
        Coin::Quarter(state) => println!("State quarter from {state:?}!"),
        _ => count += 1,
    }
}

Ou nous pourrions utiliser une expression if let et else, comme ceci : rust {{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-14-count-and-announce-if-let-else/src/main.rs:here}}

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn main() {
    let coin = Coin::Penny;
    let mut count = 0;
    if let Coin::Quarter(state) = coin {
        println!("State quarter from {state:?}!");
    } else {
        count += 1;
    }
}

Rester sur le « chemin heureux » avec let...else

Le motif courant consiste à effectuer un calcul lorsqu’une valeur est présente et à renvoyer une valeur par défaut dans le cas contraire. En poursuivant avec notre exemple de pièces avec une valeur UsState, si nous voulions dire quelque chose d’amusant en fonction de l’ancienneté de l’État sur le quarter, nous pourrions introduire une méthode sur UsState pour vérifier l’âge d’un État, comme ceci : rust {{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/listing-06-07/src/main.rs:state}}

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- snip --
        }
    }
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn describe_state_quarter(coin: Coin) -> Option<String> {
    if let Coin::Quarter(state) = coin {
        if state.existed_in(1900) {
            Some(format!("{state:?} is pretty old, for America!"))
        } else {
            Some(format!("{state:?} is relatively new."))
        }
    } else {
        None
    }
}

fn main() {
    if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
        println!("{desc}");
    }
}

Ensuite, nous pourrions utiliser if let pour filtrer sur le type de pièce, en introduisant une variable state dans le corps de la condition, comme dans l’encart 6-7.

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- snip --
        }
    }
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn describe_state_quarter(coin: Coin) -> Option<String> {
    if let Coin::Quarter(state) = coin {
        if state.existed_in(1900) {
            Some(format!("{state:?} is pretty old, for America!"))
        } else {
            Some(format!("{state:?} is relatively new."))
        }
    } else {
        None
    }
}

fn main() {
    if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
        println!("{desc}");
    }
}
Listing 6-7: Checking whether a state existed in 1900 by using conditionals nested inside an if let

Cela fait le travail, mais cela a poussé le travail dans le corps de l’instruction if let, et si le travail à faire est plus complexe, il pourrait être difficile de suivre exactement comment les branches de premier niveau sont liées. Nous pourrions aussi tirer parti du fait que les expressions produisent une valeur soit pour produire state à partir du if let, soit pour retourner prématurément, comme dans l’encart 6-8. (Vous pourriez aussi faire quelque chose de similaire avec un match.)

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- snip --
        }
    }
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn describe_state_quarter(coin: Coin) -> Option<String> {
    let state = if let Coin::Quarter(state) = coin {
        state
    } else {
        return None;
    };

    if state.existed_in(1900) {
        Some(format!("{state:?} is pretty old, for America!"))
    } else {
        Some(format!("{state:?} is relatively new."))
    }
}

fn main() {
    if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
        println!("{desc}");
    }
}
Listing 6-8: Using if let to produce a value or return early

C’est cependant un peu pénible à suivre à sa manière ! Une branche du if let produit une valeur, et l’autre retourné entièrement de la fonction.

Pour rendre ce motif courant plus agréable à exprimer, Rust propose let...else. La syntaxe let...else prend un motif à gauche et une expression à droite, de manière très similaire à if let, mais elle n’a pas de branche if, seulement une branche else. Si le motif correspond, la valeur du motif sera liée dans la portée extérieure. Si le motif ne correspond pas, le programme entrera dans le bras else, qui doit retourner de la fonction.

Dans l’encart 6-9, vous pouvez voir à quoi ressemble l’encart 6-8 lorsqu’on utilise let...else à la place de if let.

#[derive(Debug)] // so we can inspect the state in a minute
enum UsState {
    Alabama,
    Alaska,
    // --snip--
}

impl UsState {
    fn existed_in(&self, year: u16) -> bool {
        match self {
            UsState::Alabama => year >= 1819,
            UsState::Alaska => year >= 1959,
            // -- snip --
        }
    }
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn describe_state_quarter(coin: Coin) -> Option<String> {
    let Coin::Quarter(state) = coin else {
        return None;
    };

    if state.existed_in(1900) {
        Some(format!("{state:?} is pretty old, for America!"))
    } else {
        Some(format!("{state:?} is relatively new."))
    }
}

fn main() {
    if let Some(desc) = describe_state_quarter(Coin::Quarter(UsState::Alaska)) {
        println!("{desc}");
    }
}
Listing 6-9: Using let...else to clarify the flow through the function

Remarquez que de cette manière, le code reste sur le « chemin heureux » dans le corps principal de la fonction, sans avoir un flux de contrôle significativement différent pour les deux branches comme le faisait le if let.

Si vous avez une situation dans laquelle votre programme à une logique trop verbeuse à exprimer avec un match, rappelez-vous que if let et let...else font aussi partie de votre boîte à outils Rust.

Résumé

Nous avons maintenant vu comment utiliser les enums pour créer des types personnalisés qui peuvent être l’une des valeurs d’un ensemble énuméré. Nous avons montré comment le type Option<T> de la bibliothèque standard vous aide à utiliser le système de types pour prévenir les erreurs. Quand les valeurs d’enum contiennent des données, vous pouvez utiliser match ou if let pour extraire et utiliser ces valeurs, selon le nombre de cas que vous devez gérer.

Vos programmes Rust peuvent maintenant exprimer des concepts de votre domaine en utilisant des structures et des enums. Créer des types personnalisés à utiliser dans votre API garantit la sécurité des types : le compilateur s’assurera que vos fonctions ne reçoivent que des valeurs du type attendu par chaque fonction.

Afin de fournir à vos utilisateurs une API bien organisée, simple à utiliser et qui n’expose que ce dont vos utilisateurs auront besoin, tournons-nous maintenant vers les modules de Rust.