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

Gestion concise du flux d’éxécution avec if let et let...else

La syntaxe if let vous permet de combiner if et let afin de gérer les valeurs qui correspondent à un motif donné, tout en ignorant les autres. Imaginons le programme dans l’encart 6-6 qui fait un match sur la valeur Option<u8> de la variable config_max mais n’a besoin d’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!("Le maximum est réglé sur {max}"),
        _ => (),
    }
}
Listing 6-6: A match that only cares about executing code when the value is Some

Si la valeur est un Some, nous affichons la valeur dans la variante Some en associant 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 géré une seule variante, ce qui est du code inutile.

À la place, nous pourrions écrire le même programme de manière plus concise en utilisant if let. Le code suivant se comporte comme le match de l’encart 6-6 :

fn main() {
    let config_max = Some(3u8);
    if let Some(max) = config_max {
        println!("Le maximum est réglé sur {max}");
    }
}

La syntaxe if let prend un motif et une expression séparés par un signe égal. Elle fonctionne de la même manière qu’un match où l’expression est donnée au match et où le motif est sa première branche. Dans ce cas, le motif est Some(max), et le max est associé à la valeur dans le 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 la branche correspondante au match. Le code dans le bloc if let n’est exécuté que si la valeur correspond au motif.

Utiliser if let permet d’écrire moins de code, et de moins l’indenter. Cependant, vous perdez la vérification de l’exhaustivité imposée par match, qui garantit que vous n’oubliez aucun cas. Choisir entre match et if let dépend de la situation : à vous de choisir s’il vaut mieux être concis ou appliquer une vérification exhaustive.

Autrement dit, vous pouvez considérer le if let comme du sucre syntaxique pour un match qui exécute du code uniquement quand la valeur correspond à un motif donné et ignore toutes les autres valeurs.

Nous pouvons joindre un else à un if let. Le bloc de code qui va dans le else est le même que le bloc de code qui va dans le cas _ avec l’expression match. Souvenez-vous de la définition de l’énumération PieceUs de l’encart 6-4, où la variante Quarter stockait aussi une valeur EtatUs. Si nous voulions compter toutes les pièces qui ne sont pas des quarters que nous voyons passer, tout en affichant l’État des quarters, nous pourrions le faire avec une expression match comme ceci :

#[derive(Debug)]
enum EtatUs {
    Alabama,
    Alaska,
    // -- partie masquée ici --
}

enum PieceUs {
    Penny,
    Nickel,
    Dime,
    Quarter(EtatUs),
}

fn main() {
    let piece = PieceUs::Penny;
    let mut compteur = 0;
    match piece {
        PieceUs::Quarter(etat) => println!("Il s'agit d'un quarter de l'État de {etat:?} !"),
        _ => compteur += 1,
    }
}

Ou nous pourrions utiliser une expression if let/else comme ceci :

#[derive(Debug)]
enum EtatUs {
    Alabama,
    Alaska,
    // -- partie masquée ici --
}

enum PieceUs {
    Penny,
    Nickel,
    Dime,
    Quarter(EtatUs),
}

fn main() {
    let piece = PieceUs::Penny;
    let mut compteur = 0;
    if let PieceUs::Quarter(etat) = piece {
        println!("Il s'agit d'un quarter de l'État de {etat:?} !");
    } else {
        compteur += 1;
    }
}

Rester sur la « voie royale » avec let...else

Un cas courant consiste à faire un calcul lorsqu’une valeur est présente et à renvoyer une valeur par défaut dans le cas contraire. Pour reprendre notre exemple des pièces de monnaie avec une valeur EtatUs, si nous voulions faire une remarque amusante en fonction de l’ancienneté de l’État figurant sur la pièces de 25 cents, nous pourrions ajouter une méthode dans EtatUs pour vérifier l’ancienneté d’un État, comme ceci :

#[derive(Debug)] // pour pouvoir afficher l'État
enum EtatUs {
    Alabama,
    Alaska,
    // -- partie masquée ici --
}

impl EtatUs {
    fn existait_en(&self, annee: u16) -> bool {
        match self {
            EtatUs::Alabama => annee >= 1819,
            EtatUs::Alaska => annee >= 1959,
            // -- snip --
        }
    }
}

enum PieceUs {
    Penny,
    Nickel,
    Dime,
    Quarter(EtatUs),
}

fn decrit_etat_quarter(piece: PieceUs) -> Option<String> {
    if let PieceUs::Quarter(etat) = piece {
        if etat.existait_en(1900) {
            Some(format!("{etat:?} est assez vieux, pour le continent américain !"))
        } else {
            Some(format!("{etat:?} est assez jeune."))
        }
    } else {
        None
    }
}

fn main() {
    if let Some(desc) = decrit_etat_quarter(PieceUs::Quarter(EtatUs::Alaska)) {
        println!("{desc}");
    }
}

Ensuite, nous pourrions utiliser if let afin d’effectuer une vérification en fonction du type de la pièces, en introduisant une variable etat dans le corps de la condition, comme montré dans l’encart 6-7 :

#[derive(Debug)] // pour pouvoir afficher l'État
enum EtatUs {
    Alabama,
    Alaska,
    // -- partie masquée ici --
}

impl EtatUs {
    fn existait_en(&self, annee: u16) -> bool {
        match self {
            EtatUs::Alabama => annee >= 1819,
            EtatUs::Alaska => annee >= 1959,
            // -- snip --
        }
    }
}

enum PieceUs {
    Penny,
    Nickel,
    Dime,
    Quarter(EtatUs),
}

fn decrit_etat_quarter(piece: PieceUs) -> Option<String> {
    if let PieceUs::Quarter(etat) = piece {
        if etat.existait_en(1900) {
            Some(format!("{etat:?} est assez vieux, pour le continent américain !"))
        } else {
            Some(format!("{etat:?} est assez jeune."))
        }
    } else {
        None
    }
}

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

Cela permet d’atteindre l’objectif, mais ça a déplacé le traitement dans le corps de l’instruction if let, et si le traitement à effectuer est plus complexe, il peut être difficile de suivre exactement les relations entre les branches de niveau supérieur. Nous pourrions aussi tirer parti du fait que les expressions produisent une valeur, soit pour générer etat depuis le if let, soit pour sortir prématurément, comme dans l’encart 6-8 (vous pourriez faire quelque chose de similaire avec une instruction match).

#[derive(Debug)] // pour pouvoir afficher l'État
enum EtatUs {
    Alabama,
    Alaska,
    // -- partie masquée ici --
}

impl EtatUs {
    fn existait_en(&self, annee: u16) -> bool {
        match self {
            EtatUs::Alabama => annee >= 1819,
            EtatUs::Alaska => annee >= 1959,
            // -- snip --
        }
    }
}

enum PieceUs {
    Penny,
    Nickel,
    Dime,
    Quarter(EtatUs),
}

fn decrit_etat_quarter(piece: PieceUs) -> 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) = decrit_etat_quarter(PieceUs::Quarter(EtatUs::Alaska)) {
        println!("{desc}");
    }
}
Listing 6-8: Using if let to produce a value or return early

C’est quand même assez déroutant à suivre, à sa manière ! L’une des branches du if let produit une valeur, et l’autre quitte purement et simplement la fonction.

Afin de rendre ce cas courant plus agréable à exprimer, Rust a la construction let...else. La syntaxe de let...else comprend un motif du côté gauche et une expression du côté droit, de manière très similaire à if let, mais sans avoir de branche if, seulement une branche else. Si le motif ne correspond pas, le programme partira dans la branche else, qui doit quitter la fonction.

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

#[derive(Debug)] // pour pouvoir afficher l'État
enum EtatUs {
    Alabama,
    Alaska,
    // -- partie masquée ici --
}

impl EtatUs {
    fn existait_en(&self, annee: u16) -> bool {
        match self {
            EtatUs::Alabama => annee >= 1819,
            EtatUs::Alaska => annee >= 1959,
            // -- snip --
        }
    }
}

enum PieceUs {
    Penny,
    Nickel,
    Dime,
    Quarter(EtatUs),
}

fn decrit_etat_quarter(piece: PieceUs) -> 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) = decrit_etat_quarter(PieceUs::Quarter(EtatUs::Alaska)) {
        println!("{desc}");
    }
}
Listing 6-9: Using let...else to clarify the flow through the function

Notez bien qu’ainsi, on reste sur la « voie royale » dans le corps principal de la fonction, sans que le flux de contrôle diffère de manière significative entre les deux branches, contrairement à ce qui se passait avec if let.

Si vous trouvez que votre programme est alourdi par l’utilisation d’un match, souvenez-vous que if let et let...else sot aussi présents dans votre boîte à outils Rust.

Résumé

Nous avons désormais appris comment utiliser les énumérations pour créer des types personnalisés qui peuvent faire partie d’un jeu de valeurs recensées. Nous avons montré comment le type Option<T> de la bibliothèque standard vous aide à utiliser le système de types pour éviter les erreurs. Lorsque les valeurs d’énumération contiennent des données, vous pouvez utiliser match ou if let pour extraire et utiliser ces valeurs, à choisir en fonction du nombre de cas que vous voulez gérer.

Vos programmes Rust peuvent maintenant décrire des concepts métier à l’aide de structures et d’énumérations. Créer des types personnalisés à utiliser dans votre API assure la sécurité des types : le compilateur s’assurera que vos fonctions ne reçoivent que des valeurs du type attendu.

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