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

La structure 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 à une série de motifs puis d’exécuter du code en fonction du motif qui correspond. Les motifs peuvent être composés de valeurs littérales, de noms de variables, de jokers, et de bien d’autres choses ; le [chapitre 19][ch19-00-patterns] couvre tous les différents types de motifs et ce qu’ils font. La puissance de match vient de l’expressivité des motifs et du fait que le compilateur confirme que tous les cas possibles sont gérés.

Imaginez une expression match comme une machine à trier les pièces de monnaie : les pièces glissent le long d’une piste percée de trous de différentes tailles, et chaque pièce tombe dans le premier trou qu’elle rencontre et dans lequel elle passe. De la même manière, les valeurs passent à travers chaque motif d’un match, et au premier motif auquel la valeur « correspond », la valeur tombe dans le bloc de code associé pour être utilisée lors de l’exécution.

En parlant de pièces de monnaie, 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 à compter, détermine quelle pièce c’est et renvoie sa valeur en centimes, comme le montre l’encart 6-3.

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

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

fn main() {}
Listing 6-3: An enum and a match expression that has the variants of the enum as its patterns

Décomposons le match dans la fonction value_in_cents. D’abord, nous écrivons le mot-clé match suivi d’une expression, qui dans ce cas est la valeur coin. Cela semble très similaire à une expression conditionnelle utilisée avec if, mais il y à une grande différence : avec if, la condition doit s’évaluer à une valeur booléenne, mais ici elle peut être de n’importe quel type. Le type de coin dans cet exemple est l’enum Coin que nous avons définie à la première ligne.

Ensuite viennent les bras du match. Un bras a deux parties : un motif et du code. Le premier bras ici à un motif qui est la valeur Coin::Penny puis l’opérateur => qui sépare le motif et le code à exécuter. Le code dans ce cas est simplement la valeur 1. Chaque bras est séparé du suivant par une virgule.

Lorsque l’expression match s’exécute, elle compare la valeur résultante au motif de chaque bras, dans l’ordre. Si un motif correspond à la valeur, le code associé à ce motif est exécuté. Si ce motif ne correspond pas à la valeur, l’exécution continue au bras suivant, tout comme dans une machine à trier les pièces. Nous pouvons avoir autant de bras que nécessaire : dans l’encart 6-3, notre match a quatre bras.

Le code associé à chaque bras est une expression, et la valeur résultante de l’expression dans le bras correspondant est la valeur qui est renvoyée pour l’ensemble de l’expression match.

Nous n’utilisons généralement pas d’accolades si le code du bras de match est court, comme c’est le cas dans l’encart 6-3 où chaque bras renvoie simplement une valeur. Si vous voulez exécuter plusieurs lignes de code dans un bras de match, vous devez utiliser des accolades, et la virgule après le bras est alors optionnelle. Par exemple, le code suivant affiche « Lucky penny! » chaque fois que la méthode est appelée avec un Coin::Penny, mais renvoie quand même la dernière valeur du bloc, 1 : rust {{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-08-match-arm-multiple-lines/src/main.rs:here}}

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

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

fn main() {}

Les motifs qui se lient à des valeurs

Une autre fonctionnalité utile des bras de match est qu’ils peuvent se lier aux parties des valeurs qui correspondent au motif. C’est ainsi que nous pouvons extraire des valeurs des variantes d’enum.

À titre d’exemple, modifions l’une de nos variantes d’enum pour qu’elle contienne des données. De 1999 à 2008, les États-Unis ont frappé des quarters (pièces de 25 centimes) avec des designs différents pour chacun des 50 États sur une face. Aucune autre pièce n’a reçu de design d’État, donc seuls les quarters ont cette valeur supplémentaire. Nous pouvons ajouter cette information à notre enum en modifiant la variante Quarter pour qu’elle inclue une valeur UsState stockée à l’intérieur, ce que nous avons fait dans l’encart 6-4.

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

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

fn main() {}
Listing 6-4: A Coin enum in which the Quarter variant also holds a UsState value

Imaginons qu’un ami essaie de collectionner les quarters des 50 États. Pendant que nous trions notre monnaie par type de pièce, nous allons aussi annoncer le nom de l’État associé à chaque quarter afin que si c’en est un que notre ami n’a pas, il puisse l’ajouter à sa collection.

Dans l’expression match pour ce code, nous ajoutons une variable appelée state au motif qui correspond aux valeurs de la variante Coin::Quarter. Quand un Coin::Quarter correspond, la variable state se liera à la valeur de l’État de ce quarter. Ensuite, nous pouvons utiliser state dans le code de ce bras, comme ceci : rust {{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-09-variable-in-pattern/src/main.rs:here}}

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

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

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
        }
    }
}

fn main() {
    value_in_cents(Coin::Quarter(UsState::Alaska));
}

Si nous appelions value_in_cents(Coin::Quarter(UsState::Alaska)), coin serait Coin::Quarter(UsState::Alaska). Quand nous comparons cette valeur avec chacun des bras du match, aucun d’entre eux ne correspond jusqu’à ce que nous atteignions Coin::Quarter(state). À ce moment-là, la liaison pour state sera la valeur UsState::Alaska. Nous pouvons ensuite utiliser cette liaison dans l’expression println!, obtenant ainsi la valeur interne de l’État de la variante Coin pour Quarter.

Le motif match avec Option<T>

Dans la section précédente, nous voulions obtenir la valeur interne T du cas Some en utilisant Option<T> ; nous pouvons aussi gérer 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 d’Option<T>, mais le fonctionnement de l’expression match reste le même.

Disons que nous voulons écrire une fonction qui prend un Option<i32> et, s’il y à une valeur à l’intérieur, ajouté 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 d’opération.

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

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

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}
Listing 6-5: A function that uses a match expression on an Option<i32>

Examinons plus en détail la première exécution de plus_one. Lorsque nous appelons plus_one(five), la variable x dans le corps de plus_one aura la valeur Some(5). Nous comparons ensuite cette valeur à chaque bras du match : rust,ignore {{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/listing-06-05/src/main.rs:first_arm}}

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

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

La valeur Some(5) ne correspond pas au motif None, donc nous continuons au bras suivant : rust,ignore {{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/listing-06-05/src/main.rs:second_arm}}

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

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

Est-ce que Some(5) correspond à Some(i) ? Oui ! Nous avons la même variante. Le i se lie à la valeur contenue dans Some, donc i prend la valeur 5. Le code du bras de match est alors exécuté, nous ajoutons donc 1 à la valeur de i et créons une nouvelle valeur Some avec notre total 6 à l’intérieur.

Considérons maintenant le second appel de plus_one dans l’encart 6-5, où x est None. Nous entrons dans le match et comparons au premier bras : rust,ignore {{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/listing-06-05/src/main.rs:first_arm}}

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

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

Ça correspond ! Il n’y a pas de valeur à laquelle ajouter, donc le programme s’arrête et renvoie la valeur None à droite du =>. Parce que le premier bras a correspondu, aucun autre bras n’est comparé.

Combiner match et les enums est utile dans de nombreuses situations. Vous verrez beaucoup ce motif dans le code Rust : faire un match sur une enum, lier une variable aux données à l’intérieur, puis exécuter du code en fonction de cela. C’est un peu déroutant au début, mais une fois que vous vous y serez habitué, vous souhaiterez l’avoir dans tous les langages. C’est constamment un favori des utilisateurs.

Les correspondances sont exhaustives

Il y à un autre aspect de match que nous devons aborder : les motifs des bras doivent couvrir toutes les possibilités. Considérez cette version de notre fonction plus_one, qui contient un bogue et ne compilera pas : rust,ignore,does_not_compile {{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-10-non-exhaustive-match/src/main.rs:here}}

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

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

Nous n’avons pas géré le cas None, donc ce code causera un bogue. Heureusement, c’est un bogue que Rust sait détecter. Si nous essayons de compiler ce code, nous obtiendrons cette erreur : console {{#include ../listings/ch06-enums-and-pattern-matching/no-listing-10-non-exhaustive-match/output.txt}}

$ 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
  |
note: `Option<i32>` defined here
 --> /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:593:1
 ::: /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/option.rs:597:5
  |
  = note: not covered
  = 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!(),
  |

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

Rust sait que nous n’avons pas couvert tous les cas possibles et sait même quel motif nous avons oublié ! Les correspondances en Rust sont exhaustives : nous devons épuiser toutes les possibilités pour que le code soit valide. En particulier dans le cas d’Option<T>, quand Rust nous empêche d’oublier de gérer explicitement le cas None, il nous protège de l’hypothèse que nous avons une valeur quand nous pourrions avoir null, rendant ainsi impossible l’erreur à un milliard de dollars évoquée précédemment.

Les motifs attrape-tout et le caractère générique _

En utilisant des enums, nous pouvons aussi effectuer des actions spéciales pour quelques valeurs particulières, mais pour toutes les autres valeurs, effectuer une action par défaut. Imaginons que nous implémentons un jeu où, si vous lancez un 3 avec un dé, votre joueur ne bouge pas mais obtient un nouveau chapeau élégant. Si vous lancez un 7, votre joueur perd un chapeau élégant. Pour toutes les autres valeurs, votre joueur avance de ce nombre de cases 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 dépasse le cadre de cet exemple : rust {{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-15-binding-catchall/src/main.rs:here}}

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

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

Pour les deux premiers bras, les motifs sont les valeurs littérales 3 et 7. Pour le dernier bras qui couvre toutes les autres valeurs possibles, le motif est la variable que nous avons choisi de nommer other. Le code qui s’exécute pour le bras other utilise la variable en la passant à la fonction move_player.

Ce code compilé, même si nous n’avons pas listé toutes les valeurs possibles qu’un u8 peut avoir, parce que le dernier motif correspondra à toutes les valeurs non spécifiquement listées. Ce motif attrape-tout satisfait l’exigence que match doit être exhaustif. Notez que nous devons placer le bras attrape-tout en dernier car les motifs sont évalués dans l’ordre. Si nous avions placé le bras attrape-tout plus tôt, les autres bras ne s’exécuteraient jamais, donc Rust nous avertira si nous ajoutons des bras après un attrape-tout !

Rust a aussi un motif que nous pouvons utiliser lorsque nous voulons un attrape-tout mais ne voulons pas utiliser la valeur dans le motif attrape-tout : _ est un motif 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 d’une variable inutilisée.

Changeons les règles du jeu : maintenant, si vous lancez autre chose qu’un 3 ou un 7, vous devez relancer. Nous n’avons plus besoin d’utiliser la valeur attrape-tout, donc nous pouvons modifier notre code pour utiliser _ au lieu de la variable nommée other : rust {{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-16-underscore-catchall/src/main.rs:here}}

fn main() {
    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 satisfait aussi l’exigence d’exhaustivité car nous ignorons explicitement toutes les autres valeurs dans le dernier bras ; nous n’avons rien oublié.

Enfin, nous allons modifier les règles du jeu une dernière fois pour que rien d’autre ne se passe pendant votre tour si vous lancez autre chose qu’un 3 ou un 7. Nous pouvons exprimer cela en utilisant la valeur unitaire (le type tuple vide que nous avons mentionné dans la section [« Le type tuple »][tuples]) comme code associé au bras _ : rust {{#rustdoc_include ../listings/ch06-enums-and-pattern-matching/no-listing-17-underscore-unit/src/main.rs:here}}

fn main() {
    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 d’autre valeur qui ne correspond pas à un motif dans un bras précédent, et que nous ne voulons exécuter aucun code dans ce cas.

Il y a davantage à dire sur les motifs et le filtrage, que nous couvrirons au chapitre 19. Pour le moment, nous allons passer à la syntaxe if let, qui peut être utile dans des situations où l’expression match est un peu verbeuse.