La structure de contrôle de flux `match`

RustRustBeginner
Pratiquer maintenant

This tutorial is from open-source community. Access the source code

💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici

Introduction

Bienvenue dans The Match Control Flow Construct. Ce laboratoire est une partie du Rust Book. Vous pouvez pratiquer vos compétences Rust dans LabEx.

Dans ce laboratoire, nous allons explorer la puissante construction de flux de contrôle match en Rust, qui permet la correspondance de modèles et l'exécution de code sur la base du modèle correspondant.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/operator_overloading("Traits for Operator Overloading") subgraph Lab Skills rust/variable_declarations -.-> lab-100399{{"La structure de contrôle de flux `match`"}} rust/integer_types -.-> lab-100399{{"La structure de contrôle de flux `match`"}} rust/function_syntax -.-> lab-100399{{"La structure de contrôle de flux `match`"}} rust/expressions_statements -.-> lab-100399{{"La structure de contrôle de flux `match`"}} rust/method_syntax -.-> lab-100399{{"La structure de contrôle de flux `match`"}} rust/operator_overloading -.-> lab-100399{{"La structure de contrôle de flux `match`"}} end

La construction de flux de contrôle match

Rust possède une construction de flux de contrôle extrêmement puissante appelée match qui vous permet de comparer une valeur avec une série de modèles et d'exécuter ensuite du code en fonction du modèle qui correspond. Les modèles peuvent être composés de valeurs littérales, de noms de variables, de jokers et de nombreuses autres choses ; le chapitre 18 couvre tous les différents types de modèles et ce qu'ils font. Le pouvoir de match vient de l'expressivité des modèles et du fait que le compilateur confirme que tous les cas possibles sont traités.

Imaginez une expression match comme une machine de tri de pièces : les pièces glissent le long d'une piste avec des trous de tailles différentes, et chaque pièce tombe dans le premier trou qu'elle rencontre et dans lequel elle rentre. De la même manière, les valeurs passent par chaque modèle dans une match, et au premier modèle auquel la valeur "convient", la valeur tombe dans le bloc de code associé pour être utilisé pendant l'exécution.

Parlant de pièces, utilisons-les comme exemple avec match! Nous pouvons écrire une fonction qui prend une pièce américaine inconnue et, de manière similaire à la machine de comptage, détermine laquelle elle est et renvoie sa valeur en cents, comme montré dans la liste 6-3.

1 enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
  2 match coin {
      3 Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

Liste 6-3 : Un enum et une expression match qui a les variantes de l'enum comme modèles

Analysons la match dans la fonction value_in_cents. Tout d'abord, nous listons le mot clé match suivi d'une expression, qui dans ce cas est la valeur coin [2]. Cela semble très similaire à une expression utilisée avec if, mais il y a une grande différence : avec if, l'expression doit renvoyer une valeur booléenne, mais ici elle peut renvoyer n'importe quel type. Le type de coin dans cet exemple est l'enum Coin que nous avons défini à [1].

Ensuite, viennent les branches de la match. Une branche a deux parties : un modèle et du code. La première branche ici a un modèle qui est la valeur Coin::Penny puis l'opérateur => qui sépare le modèle et le code à exécuter [3]. Le code dans ce cas est juste la valeur 1. Chaque branche est séparée de la suivante par une virgule.

Lorsque l'expression match s'exécute, elle compare la valeur résultante avec le modèle de chaque branche, dans l'ordre. Si un modèle correspond à la valeur, le code associé à ce modèle est exécuté. Si ce modèle ne correspond pas à la valeur, l'exécution continue avec la branche suivante, tout comme dans une machine de tri de pièces. Nous pouvons avoir autant de branches que nécessaire : dans la liste 6-3, notre match a quatre branches.

Le code associé à chaque branche est une expression, et la valeur résultante de l'expression dans la branche correspondante est la valeur qui est renvoyée pour l'expression match entière.

Nous n'utilisons généralement pas les accolades si le code de la branche de la match est court, comme c'est le cas dans la liste 6-3 où chaque branche ne renvoie qu'une valeur. Si vous voulez exécuter plusieurs lignes de code dans une branche de la match, vous devez utiliser les accolades, et la virgule suivant la branche est alors facultative. Par exemple, le code suivant affiche "Pièce de centime porte-bonheur!" chaque fois que la méthode est appelée avec une Coin::Penny, mais renvoie toujours la dernière valeur du bloc, 1 :

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

Modèles qui se lient à des valeurs

Une autre fonctionnalité pratique des branches de match est qu'elles peuvent se lier aux parties des valeurs qui correspondent au modèle. C'est ainsi que nous pouvons extraire des valeurs des variantes d'enumération.

En guise d'exemple, modifions l'une de nos variantes d'enumération pour qu'elle contienne des données à l'intérieur. De 1999 à 2008, la monnaie américaine a frappé des quarters avec des dessins différents pour chacune des 50 états américains sur un côté. Aucune autre pièce n'a reçu des dessins d'états, donc seul le quarter a cette valeur supplémentaire. Nous pouvons ajouter cette information à notre enum en changeant la variante Quarter pour inclure une valeur UsState stockée à l'intérieur, ce que nous avons fait dans la liste 6-4.

#[derive(Debug)] // afin que nous puissions examiner l'état dans un instant
enum UsState {
    Alabama,
    Alaska,
    --snip--
}

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

Liste 6-4 : Un enum Coin dans lequel la variante Quarter contient également une valeur UsState

Imaginons qu'un ami essaye de collecter les 50 quarters d'états. Pendant que nous trions notre monnaie de poche par type de pièce, nous allons également annoncer le nom de l'état associé à chaque quarter afin que si c'est un état que notre ami n'a pas, il puisse l'ajouter à sa collection.

Dans l'expression match de ce code, nous ajoutons une variable appelée state au modèle qui correspond aux valeurs de la variante Coin::Quarter. Lorsqu'un Coin::Quarter correspond, la variable state se lira à la valeur de l'état de ce quarter. Ensuite, nous pouvons utiliser state dans le code de cette branche, comme ceci :

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {:?}!", state);
            25
        }
    }
}

Si nous appelions value_in_cents(Coin::Quarter(UsState::Alaska)), coin serait Coin::Quarter(UsState::Alaska). Lorsque nous comparons cette valeur avec chacune des branches de match, aucune d'entre elles ne correspond jusqu'à ce que nous arrivions à Coin::Quarter(state). A ce moment-là, la liaison pour state sera la valeur UsState::Alaska. Nous pouvons ensuite utiliser cette liaison dans l'expression println!, permettant ainsi d'obtenir la valeur d'état interne de la variante Coin pour Quarter.

Correspondance avec Option<T>{=html}

Dans la section précédente, nous voulions extraire la valeur interne T dans le cas Some lorsqu'on utilise Option<T> ; nous pouvons également traiter Option<T> avec match, comme nous l'avons fait avec l'enum Coin! Au lieu de comparer des pièces, nous allons comparer les variantes de Option<T>, mais la manière dont fonctionne l'expression match reste la même.

Disons que nous voulons écrire une fonction qui prend une Option<i32> et, 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 devrait renvoyer la valeur None et ne pas tenter d'effectuer aucune opération.

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

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

let five = Some(5);
let six = plus_one(five); 3
let none = plus_one(None); 4

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

Examnons plus en détail la première exécution de plus_one. Lorsque nous appelons plus_one(five) [3], la variable x dans le corps de plus_one aura la valeur Some(5). Nous comparons ensuite cela avec chaque branche de correspondance :

None => None,

La valeur Some(5) ne correspond pas au modèle None [1], donc nous passons à la branche suivante :

Some(i) => Some(i + 1),

Some(5) correspond-il à Some(i) [2]? Oui, bien sûr! Nous avons la même variante. La variable i se lie à la valeur contenue dans Some, donc i prend la valeur 5. Le code de la branche de correspondance est ensuite exécuté, donc nous ajoutons 1 à la valeur de i et créons une nouvelle valeur Some avec notre total 6 à l'intérieur.

Maintenant, considérons le second appel de plus_one dans la liste 6-5, où x est None [4]. Nous entrons dans la match et comparons avec la première branche [1].

Elle correspond! Il n'y a pas de valeur à laquelle ajouter, donc le programme s'arrête et renvoie la valeur None du côté droit de =>. Comme la première branche a correspondu, aucune autre branche n'est comparée.

La combinaison de match et d'enums est utile dans de nombreuses situations. Vous verrez ce modèle très souvent dans le code Rust : effectuer une correspondance sur un enum, lier une variable aux données à l'intérieur, puis exécuter du code en fonction de cela. C'est un peu difficile au début, mais une fois que vous vous y êtes habitué, vous souhaiterez l'avoir dans toutes les langues. C'est constamment un favori des utilisateurs.

Les correspondances sont exhaustives

Il y a un autre aspect de match que nous devons discuter : les modèles des branches doivent couvrir toutes les possibilités. Considérez cette version de notre fonction plus_one, qui contient un bogue et ne compilera pas :

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

Nous n'avons pas traité le cas None, donc ce code entraînera un bogue. Heureusement, c'est un bogue que Rust sait détecter. Si nous essayons de compiler ce code, nous obtiendrons cette erreur :

error[E0004]: non-exhaustive patterns: `None` not covered
 --> src/main.rs:3:15
  |
3 |         match x {
  |               ^ pattern `None` not covered
  |
  note: `Option<i32>` defined here
      = note: the matched value is of type `Option<i32>`
help: ensure that all possible cases are being handled by adding
a match arm with a wildcard pattern or an explicit pattern as
shown
    |
4   ~             Some(i) => Some(i + 1),
5   ~             None => todo!(),
    |

Rust sait que nous n'avons pas couvert toutes les possibilités et même lequel des modèles nous avons oublié! Les correspondances en Rust sont exhaustives : nous devons épuiser toutes les dernières possibilités pour que le code soit valide. En particulier dans le cas de Option<T>, lorsque Rust nous empêche d'oublier de traiter explicitement le cas None, il nous protège d'assumer qu'il existe une valeur alors que nous pourrions avoir null, rendant ainsi impossible la fameuse erreur coûteuse évoquée précédemment.

Modèles génériques et le placeholder _

En utilisant les enums, nous pouvons également prendre des actions spécifiques pour quelques valeurs particulières, mais pour toutes les autres valeurs, prendre une action par défaut. Imaginez que nous implémentons un jeu où, si vous obtenez un 3 lors d'un lancer de dé, votre joueur ne bouge pas, mais reçoit au contraire un nouveau chapeau élégant. Si vous obtenez un 7, votre joueur perd un chapeau élégant. Pour toutes les autres valeurs, votre joueur avance de ce nombre d'espaces sur le plateau de jeu. Voici un match qui implémente cette logique, avec le résultat du lancer de dé codé en dur plutôt qu'une valeur aléatoire, et toute autre logique représentée par des fonctions sans corps car leur implémentation réelle est hors du champ d'application de cet exemple :

let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
  1 other => move_player(other),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}

Pour les deux premières branches, les modèles sont les valeurs littérales 3 et 7. Pour la dernière branche qui couvre toutes les autres valeurs possibles, le modèle est la variable que nous avons choisi de nommer other [1]. Le code qui s'exécute pour la branche other utilise la variable en la passant à la fonction move_player.

Ce code compile, même si nous n'avons pas listé toutes les valeurs possibles qu'un u8 peut avoir, car le dernier modèle correspondra à toutes les valeurs qui ne sont pas spécifiquement listées. Ce modèle générique répond à la condition que le match doit être exhaustif. Notez que nous devons placer la branche générique en dernier car les modèles sont évalués dans l'ordre. Si nous plaçons la branche générique plus tôt, les autres branches ne s'exécuteraient jamais, donc Rust nous avertira si nous ajoutons des branches après une branche générique!

Rust dispose également d'un modèle que nous pouvons utiliser lorsque nous voulons un modèle générique mais ne voulons pas utiliser la valeur dans le modèle générique : _ est un modèle spécial qui correspond à n'importe quelle valeur et ne se lie pas à cette valeur. Cela indique à Rust que nous n'allons pas utiliser la valeur, donc Rust ne nous avertira pas au sujet d'une variable inutilisée.

Modifions les règles du jeu : maintenant, si vous obtenez n'importe quoi d'autre qu'un 3 ou un 7, vous devez relancer le dé. Nous n'avons plus besoin d'utiliser la valeur générique, donc nous pouvons modifier notre code pour utiliser _ au lieu de la variable nommée other :

let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    _ => reroll(),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}

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

Enfin, modifions les règles du jeu une dernière fois de sorte que rien d'autre ne se passe pendant votre tour si vous obtenez n'importe quoi d'autre qu'un 3 ou un 7. Nous pouvons l'exprimer en utilisant la valeur unitaire (le type de tuple vide que nous avons mentionné dans "Le type tuple") comme code qui accompagne la branche _ :

let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    _ => (),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}

Ici, nous disons explicitement à Rust que nous n'allons pas utiliser n'importe quelle autre valeur qui ne correspond pas à un modèle dans une branche précédente, et que nous ne voulons pas exécuter de code dans ce cas.

Il y a plus à savoir sur les modèles et la correspondance que nous aborderons au chapitre 18. Pour l'instant, nous allons passer à la syntaxe if let, qui peut être utile dans des situations où l'expression match est un peu verbeuse.

Sommaire

Félicitations! Vous avez terminé le laboratoire sur la structure de contrôle de flux match. Vous pouvez pratiquer d'autres laboratoires dans LabEx pour améliorer vos compétences.