La structure de contrôle de flux match

Rust a une structure de contrôle de flux très puissante appelée match qui vous permet de comparer une valeur avec une série de motifs et d'exécuter du code en fonction du motif qui correspond. Les motifs peuvent être constitués de valeurs littérales, de noms de variables, de jokers, parmi tant d'autres ; le chapitre 18 va couvrir tous les différents types de motifs et ce qu'ils font. Ce qui fait la puissance de match est l'expressivité des motifs et le fait que le compilateur vérifie que tous les cas possibles sont bien gérés.

Considérez l'expression match comme une machine à trier les pièces de monnaie : les pièces descendent le long d'une piste avec des trous de tailles différentes, et chaque pièce tombe dans le premier trou à sa taille qu'elle rencontre. De manière similaire, les valeurs parcourent tous les motifs dans un match, et au premier motif auquel la valeur “correspond”, la valeur va descendre dans le bloc de code correspondant afin d'être utilisée pendant son exécution. En parlant des pièces, utilisons-les avec un exemple qui utilise match ! Nous pouvons écrire une fonction qui prend en paramètre une pièce inconnue des États-Unis d'Amérique et qui peut, de la même manière qu'une machine à trier, déterminer quelle pièce c'est et retourner sa valeur en centimes, comme ci-dessous dans l'encart 6-3.

enum PieceUs {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn valeur_en_centimes(piece: PieceUs) -> u8 {
    match piece {
        PieceUs::Penny => 1,
        PieceUs::Nickel => 5,
        PieceUs::Dime => 10,
        PieceUs::Quarter => 25,
    }
}

fn main() {}

Encart 6-3 : Une énumération et une expression match qui trie les variantes de l'énumération dans ses motifs

Décomposons le match dans la fonction valeur_en_centimes. En premier lieu, nous utilisons le mot-clé match suivi par une expression, qui dans notre cas est la valeur de piece. Cela ressemble beaucoup à une expression utilisée avec if, mais il y a une grosse différence : avec if, l'expression doit retourner une valeur booléenne, mais ici, elle retourne n'importe quel type. Dans cet exemple, piece est de type PieceUs, qui est l'énumération que nous avons définie à la première ligne.

Ensuite, nous avons les branches du match. Une branche a deux parties : un motif et du code. La première branche a ici pour motif la valeur PieceUs::Penny et ensuite l'opérateur => qui sépare le motif et le code à exécuter. Le code dans ce cas est uniquement la valeur 1. Chaque branche est séparée de la suivante par une virgule.

Lorsqu'une expression match est exécutée, elle compare la valeur de piece avec le motif de chaque branche, dans l'ordre. Si un motif correspond à la valeur, le code correspondant à ce motif est alors exécuté. Si ce motif ne correspond pas à la valeur, l'exécution passe à la prochaine branche, un peu comme dans une machine de tri de pièces. Nous pouvons avoir autant de branches que nécessaire : dans l'encart 6-3, notre match a quatre branches.

Le code correspondant à chaque branche est une expression, et la valeur qui résulte de l'expression dans la branche correspondante est la valeur qui sera retournée par l'expression match.

Habituellement, nous n'utilisons pas les accolades si le code de la branche correspondante est court, comme c'est le cas dans l'encart 6-3 où chaque branche retourne simplement une valeur. Si vous voulez exécuter plusieurs lignes de code dans une branche d'un match, vous devez utiliser les accolades. Par exemple, le code suivant va afficher “Un centime porte-bonheur !” à chaque fois que la méthode est appelée avec une valeur PieceUs::Penny, mais va continuer à retourner la dernière valeur du bloc, 1 :

enum PieceUs {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn valeur_en_centimes(piece: PieceUs) -> u8 {
    match piece {
        PieceUs::Penny => {
            println!("Un centime porte-bonheur !");
            1
        }
        PieceUs::Nickel => 5,
        PieceUs::Dime => 10,
        PieceUs::Quarter => 25,
    }
}

fn main() {}

Des motifs reliés à des valeurs

Une autre fonctionnalité intéressante des branches de match est qu'elles peuvent se lier aux valeurs qui correspondent au motif. C'est ainsi que nous pouvons extraire des valeurs d'une variante d'énumération.

En guise d'exemple, changeons une de nos variantes d'énumération pour stocker une donnée à l'intérieur. Entre 1999 et 2008, les États-Unis d'Amérique ont frappé un côté des quarters (pièces de 25 centimes) avec des dessins différents pour chacun des 50 États. Les autres pièces n'ont pas eu de dessins d'États, donc seul le quarter a cette valeur en plus. Nous pouvons ajouter cette information à notre enum en changeant la variante Quarter pour y ajouter une valeur EtatUs qui y sera stockée à l'intérieur, comme nous l'avons fait dans l'encart 6-4.

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

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

fn main() {}

Encart 6-4 : Une énumération PieceUs dans laquelle la variante Quarter stocke en plus une valeur de type EtatUs

Imaginons qu'un de vos amis essaye de collectionner tous les quarters des 50 États. Pendant que nous trions notre monnaie en vrac par type de pièce, nous mentionnerons aussi le nom de l'État correspondant à chaque quarter de sorte que si notre ami ne l'a pas, il puisse l'ajouter à sa collection.

Dans l'expression match de ce code, nous avons ajouté une variable etat au motif qui correspond à la variante PieceUs::Quarter. Quand on aura une correspondance PieceUs::Quarter, la variable etat sera liée à la valeur de l'État de cette pièce. Ensuite, nous pourrons utiliser etat dans le code de cette branche, comme ceci :

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

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

fn valeur_en_centimes(piece: PieceUs) -> u8 {
    match piece {
        PieceUs::Penny => 1,
        PieceUs::Nickel => 5,
        PieceUs::Dime => 10,
        PieceUs::Quarter(etat) => {
            println!("Il s'agit d'un quarter de l'État de {:?} !", etat);
            25
        },
    }
}

fn main() {
    valeur_en_centimes(PieceUs::Quarter(EtatUs::Alaska));
}

Si nous appelons valeur_en_centimes(PieceUs::Quarter(EtatUs::Alaska)), piece vaudra PieceUs::Quarter(EtatUs::Alaska). Quand nous comparons cette valeur avec toutes les branches du match, aucune d'entre elles ne correspondra jusqu'à ce qu'on arrive à PieceUs::Quarter(etat). À partir de ce moment, la variable etat aura la valeur EtatUs::Alaska. Nous pouvons alors utiliser cette variable dans l'expression println!, ce qui nous permet d'afficher la valeur de l'État à l'intérieur de la variante Quarter de l'énumération PieceUs.

Utiliser match avec Option<T>

Dans la section précédente, nous voulions obtenir la valeur interne T dans le cas de Some lorsqu'on utilisait Option<T> ; nous pouvons aussi gérer les Option<T> en utilisant match comme nous l'avons fait avec l'énumération PieceUs ! Au lieu de comparer des pièces, nous allons comparer les variantes de Option<T>, mais la façon d'utiliser l'expression match reste la même.

Disons que nous voulons écrire une fonction qui prend une Option<i32> et qui, s'il y a une valeur à l'intérieur, ajoute 1 à cette valeur. S'il n'y a pas de valeur à l'intérieur, la fonction retournera la valeur None et ne va rien faire de plus.

Cette fonction est très facile à écrire, grâce à match, et ressemblera à l'encart 6-5.

fn main() {
    fn plus_un(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let cinq = Some(5);
    let six = plus_un(cinq);
    let none = plus_un(None);
}

Encart 6-5 : Une fonction qui utilise une expression match sur une Option<i32>

Examinons la première exécution de plus_un en détail. Lorsque nous appelons plus_un(cinq), la variable x dans le corps de plus_un aura la valeur Some(5). Ensuite, nous comparons cela à chaque branche du match.

fn main() {
    fn plus_un(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let cinq = Some(5);
    let six = plus_un(cinq);
    let none = plus_un(None);
}

La valeur Some(5) ne correspond pas au motif None, donc nous continuons à la branche suivante.

fn main() {
    fn plus_un(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let cinq = Some(5);
    let six = plus_un(cinq);
    let none = plus_un(None);
}

Est-ce que Some(5) correspond au motif Some(i) ? Bien sûr ! Nous avons la même variante. Le i va prendre la valeur contenue dans le Some, donc i prend la valeur 5. Le code dans la branche du match est exécuté, donc nous ajoutons 1 à la valeur de i et nous créons une nouvelle valeur Some avec notre résultat 6 à l'intérieur.

Maintenant, regardons le second appel à plus_un dans l'encart 6-5, où x vaut None. Nous entrons dans le match et nous le comparons à la première branche.

fn main() {
    fn plus_un(x: Option<i32>) -> Option<i32> {
        match x {
            None => None,
            Some(i) => Some(i + 1),
        }
    }

    let cinq = Some(5);
    let six = plus_un(cinq);
    let none = plus_un(None);
}

Cela correspond ! Il n'y a pas de valeur à additionner, donc le programme s'arrête et retourne la valeur None qui est dans le côté droit du =>. Comme la première branche correspond, les autres branches ne sont pas comparées.

La combinaison de match et des énumérations est utile dans de nombreuses situations. Vous allez revoir de nombreuses fois ce schéma dans du code Rust : utiliser match sur une énumération, récupérer la valeur qu'elle renferme, et exécuter du code en fonction de sa valeur. C'est un peu délicat au début, mais une fois que vous vous y êtes habitué, vous regretterez de ne pas l'avoir dans les autres langages. Cela devient toujours l'outil préféré de ses utilisateurs.

Les match sont toujours exhaustifs

Il y a un autre point de match que nous devons aborder. Examinez cette version de notre fonction plus_un qui a un bogue et ne va pas se compiler :

fn main() {
    fn plus_un(x: Option<i32>) -> Option<i32> {
        match x {
            Some(i) => Some(i + 1),
        }
    }

    let cinq = Some(5);
    let six = plus_un(cinq);
    let none = plus_un(None);
}

Nous n'avons pas géré le cas du None, donc ce code va générer un bogue. Heureusement, c'est un bogue que Rust sait gérer. Si nous essayons de compiler ce code, nous allons obtenir cette erreur :

$ cargo run
   Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
   --> src/main.rs:3:15
    |
3   |         match x {
    |               ^ pattern `None` not covered
    |
    = help: ensure that all possible cases are being handled, possibly by adding wildcards or more match arms
    = note: the matched value is of type `Option<i32>`

For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` due to previous error

Rust sait que nous n'avons pas couvert toutes les possibilités et sait même quel motif nous avons oublié ! Les match de Rust sont exhaustifs : nous devons traiter toutes les possibilités afin que le code soit valide. C'est notamment le cas avec Option<T> : quand Rust nous empêche d'oublier de gérer explicitement le cas de None, il nous protège d'une situation où nous supposons que nous avons une valeur alors que nous pourrions avoir null, ce qui rend impossible l'erreur à un milliard de dollars que nous avons vue précédemment.

Les motifs génériques et le motif _

En utilisant les énumérations, nous pouvons aussi appliquer des actions spéciales pour certaines valeurs précises, mais une action par défaut pour toutes les autres valeurs. Imaginons que nous implémentons un jeu dans lequel, si vous obtenez une valeur de 3 sur un lancé de dé, votre joueur ne se déplace pas, mais à la place il obtient un nouveau chapeau fataisie. Si vous obtenez un 7, votre joueur perd son chapeau fantaisie. Pour toutes les autres valeurs, votre joueur se déplace de ce nombre de cases sur le plateau du jeu. Voici un match qui implémente cette logique, avec le résultat du lancé de dé codé en dur plutôt qu'issu d'une génération aléatoire, et toute la logique des autres fonctions sont des corps vides car leur implémentation n'est pas le sujet de cet exemple :

fn main() {
    let jete_de_de = 9;
    match jete_de_de {
        3 => ajouter_chapeau_fantaisie(),
        7 => enleve_chapeau_fantaisie(),
        autre => deplace_joueur(autre),
    }

    fn ajouter_chapeau_fantaisie() {}
    fn enleve_chapeau_fantaisie() {}
    fn deplace_joueur(nombre_cases: u8) {}
}

Dans les deux premières branches, les motifs sont les valeurs litérales 3 et 7. La dernière branche couvre toutes les autres valeurs possibles, le motif est la variable autre. Le code qui s'exécute pour la branche autre utilise la variable en la passant dans la fonction deplacer_joueur.

Ce code se compile, même si nous n'avons pas listé toutes les valeurs possibles qu'un u8 puisse avoir, car le dernier motif va correspondre à toutes les valeurs qui ne sont pas spécifiquement listés. Ce motif générique répond à la condition qu'un match doive être exhaustif. Notez que nous devons placer la branche avec le motif générique en tout dernier, car les motifs sont évalués dans l'ordre. Rust va nous prévenir si nous ajoutons des branches après un motif générique car toutes ces autres branches ne seront jamais vérifiées !

Rust a aussi un motif que nous pouvons utiliser lorsque nous n'avons pas besoin d'utiliser la valeur dans le motif générique : _, qui est un motif spécial qui vérifie n'importe quelle valeur et ne récupère pas cette valeur. Ceci indique à Rust que nous n'allons pas utiliser la valeur, donc Rust ne va pas nous prévenir qu'il y a une variable non utilisée.

Changeons les règles du jeu pour que si nous obtenions autre chose qu'un 3 ou un 7, nous jetions à nouveau le dé. Nous n'avons pas besoin d'utiliser la valeur dans ce cas, donc nous pouvons changer notre code pour utiliser _ au lieu de la variable autre :

fn main() {
    let jete_de_de = 9;
    match jete_de_de {
        3 => ajouter_chapeau_fantaisie(),
        7 => enleve_chapeau_fantaisie(),
        _ => relancer(),
    }

    fn ajouter_chapeau_fantaisie() {}
    fn enleve_chapeau_fantaisie() {}
    fn relancer() {}
}

Cet exemple répond bien aux critères d'exhaustivité car nous ignorons explicitement toutes les autres valeurs dans la dernière branche ; nous n'avons rien oublié.

Si nous changeons à nouveau les règles du jeu, afin que rien se passe si vous obtenez autre chose qu'un 3 ou un 7, nous pouvons exprimer cela en utilisant la valeur unité (le type tuple vide que nous avons cité dans une section précédente) dans le code de la branche _ :

fn main() {
    let jete_de_de = 9;
    match jete_de_de {
        3 => ajouter_chapeau_fantaisie(),
        7 => enleve_chapeau_fantaisie(),
        _ => (),
    }

    fn ajouter_chapeau_fantaisie() {}
    fn enleve_chapeau_fantaisie() {}
}

Ici, nous indiquons explicitement à Rust que nous n'allons pas utiliser d'autres valeurs qui ne correspondent pas à un motif des branches antérieures, et nous ne voulons lancer aucun code dans ce cas.

Il existe aussi d'autres motifs que nous allons voir dans le chapitre 18. Pour l'instant, nous allons voir l'autre syntaxe if let, qui peut se rendre utile dans des cas où l'expression match est trop verbeuse.