Pratiques de la syntaxe des motifs Rust

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 Pattern Syntax. Ce laboratoire est une partie du Rust Book. Vous pouvez pratiquer vos compétences Rust dans LabEx.

Dans ce laboratoire, nous discutons de la syntaxe valide dans les motifs et fournissons des exemples de cas où et pourquoi vous voudriez utiliser chacun d'entre eux.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/MemorySafetyandManagementGroup(["Memory Safety and Management"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/BasicConceptsGroup -.-> rust/mutable_variables("Mutable Variables") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/DataTypesGroup -.-> rust/boolean_type("Boolean Type") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/MemorySafetyandManagementGroup -.-> rust/lifetime_specifiers("Lifetime Specifiers") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") subgraph Lab Skills rust/variable_declarations -.-> lab-100446{{"Pratiques de la syntaxe des motifs Rust"}} rust/mutable_variables -.-> lab-100446{{"Pratiques de la syntaxe des motifs Rust"}} rust/integer_types -.-> lab-100446{{"Pratiques de la syntaxe des motifs Rust"}} rust/boolean_type -.-> lab-100446{{"Pratiques de la syntaxe des motifs Rust"}} rust/string_type -.-> lab-100446{{"Pratiques de la syntaxe des motifs Rust"}} rust/function_syntax -.-> lab-100446{{"Pratiques de la syntaxe des motifs Rust"}} rust/expressions_statements -.-> lab-100446{{"Pratiques de la syntaxe des motifs Rust"}} rust/lifetime_specifiers -.-> lab-100446{{"Pratiques de la syntaxe des motifs Rust"}} rust/method_syntax -.-> lab-100446{{"Pratiques de la syntaxe des motifs Rust"}} end

Pattern Syntax

Dans cette section, nous rassemblons toute la syntaxe valide dans les motifs et discutons pourquoi et quand vous voudriez utiliser chacun d'entre eux.

Correspondance avec des littéraux

Comme vous l'avez vu au chapitre 6, vous pouvez correspondre des motifs avec des littéraux directement. Le code suivant donne quelques exemples :

Nom de fichier : src/main.rs

let x = 1;

match x {
    1 => println!("one"),
    2 => println!("two"),
    3 => println!("three"),
    _ => println!("anything"),
}

Ce code affiche one car la valeur de x est 1. Cette syntaxe est utile lorsque vous voulez que votre code prenne une action si elle reçoit une valeur concrète particulière.

Correspondance avec des variables nommées

Les variables nommées sont des motifs irréfutables qui correspondent à n'importe quelle valeur, et nous les avons utilisées à de nombreuses reprises dans ce livre. Cependant, il y a une complication lorsqu'on utilise des variables nommées dans des expressions match. Étant donné que match démarre un nouveau contexte, les variables déclarées en tant que partie d'un motif à l'intérieur de l'expression match ombrage les variables du même nom en dehors de la structure match, comme c'est le cas pour toutes les variables. Dans la Liste 18-11, nous déclarons une variable nommée x avec la valeur Some(5) et une variable y avec la valeur 10. Nous créons ensuite une expression match sur la valeur x. Considérez les motifs dans les branches match et l'instruction println! à la fin, et essayez de déterminer ce que le code affichera avant d'exécuter ce code ou de continuer à lire.

Nom de fichier : src/main.rs

fn main() {
  1 let x = Some(5);
  2 let y = 10;

    match x {
      3 Some(50) => println!("Got 50"),
      4 Some(y) => println!("Matched, y = {y}"),
      5 _ => println!("Default case, x = {:?}", x),
    }

  6 println!("at the end: x = {:?}, y = {y}", x);
}

Liste 18-11 : Une expression match avec une branche qui introduit une variable y ombragée

Examillons ce qui se passe lorsque l'expression match est exécutée. Le motif dans la première branche match [3] ne correspond pas à la valeur définie de x [1], donc le code continue.

Le motif dans la deuxième branche match [4] introduit une nouvelle variable nommée y qui correspondra à n'importe quelle valeur à l'intérieur d'une valeur Some. Étant donné que nous sommes dans un nouveau contexte à l'intérieur de l'expression match, il s'agit d'une nouvelle variable y, pas de la variable y que nous avons déclarée au début avec la valeur 10 [2]. Cette nouvelle liaison y correspondra à n'importe quelle valeur à l'intérieur d'un Some, ce qui est ce que nous avons dans x. Par conséquent, cette nouvelle y se lie à la valeur interne du Some dans x. Cette valeur est 5, donc l'expression de cette branche est exécutée et affiche Matched, y = 5.

Si x avait été une valeur None au lieu de Some(5), les motifs dans les deux premières branches n'auraient pas correspondu, donc la valeur aurait été associée au tiret de soulignement [5]. Nous n'avons pas introduit la variable x dans le motif de la branche du tiret de soulignement, donc la x dans l'expression est toujours la x externe qui n'a pas été ombragée. Dans ce cas hypothétique, le match afficherait Default case, x = None.

Lorsque l'expression match est terminée, son contexte se termine, ainsi que celui de la variable y interne. La dernière instruction println! [6] produit at the end: x = Some(5), y = 10.

Pour créer une expression match qui compare les valeurs de x externe et y, plutôt qu'introduire une variable ombragée, nous devrions utiliser une condition de garde match à la place. Nous parlerons des gardes match dans "Conditions supplémentaires avec les gardes match".

Plusieurs motifs

Dans les expressions match, vous pouvez correspondre plusieurs motifs en utilisant la syntaxe |, qui est l'opérateur ou de motif. Par exemple, dans le code suivant, nous correspondons la valeur de x aux branches match, dont la première a une option ou, ce qui signifie que si la valeur de x correspond à l'une des valeurs de cette branche, le code de cette branche sera exécuté :

Nom de fichier : src/main.rs

let x = 1;

match x {
    1 | 2 => println!("one or two"),
    3 => println!("three"),
    _ => println!("anything"),
}

Ce code affiche one or two.

Correspondance de plages de valeurs avec..=

La syntaxe ..= nous permet de correspondre à une plage de valeurs inclusive. Dans le code suivant, lorsqu'un motif correspond à l'une des valeurs dans la plage donnée, cette branche sera exécutée :

Nom de fichier : src/main.rs

let x = 5;

match x {
    1..=5 => println!("one through five"),
    _ => println!("something else"),
}

Si x est 1, 2, 3, 4 ou 5, la première branche correspondra. Cette syntaxe est plus pratique pour plusieurs valeurs de correspondance que d'utiliser l'opérateur | pour exprimer la même idée ; si nous utilisions |, nous devrions spécifier 1 | 2 | 3 | 4 | 5. Spécifier une plage est beaucoup plus court, surtout si nous voulons correspondre, disons, n'importe quel nombre entre 1 et 1 000!

Le compilateur vérifie que la plage n'est pas vide à la compilation, et puisque les seuls types pour lesquels Rust peut dire si une plage est vide ou non sont char et les valeurs numériques, les plages ne sont autorisées que avec des valeurs numériques ou char.

Voici un exemple utilisant des plages de valeurs de char :

Nom de fichier : src/main.rs

let x = 'c';

match x {
    'a'..='j' => println!("early ASCII letter"),
    'k'..='z' => println!("late ASCII letter"),
    _ => println!("something else"),
}

Rust peut dire que 'c' est dans la plage du premier motif et affiche early ASCII letter.

Déstruction pour séparer les valeurs

Nous pouvons également utiliser des motifs pour décomposer des structs, des enum et des tuples afin d'utiliser différentes parties de ces valeurs. Passons en revue chaque type de valeur.

Déconstruction de structs

La Liste 18-12 montre une struct Point avec deux champs, x et y, que nous pouvons séparer à l'aide d'un motif avec une instruction let.

Nom de fichier : src/main.rs

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x: a, y: b } = p;
    assert_eq!(0, a);
    assert_eq!(7, b);
}

Liste 18-12 : Déconstruction des champs d'une struct en variables séparées

Ce code crée les variables a et b qui correspondent aux valeurs des champs x et y de la struct p. Cet exemple montre que les noms des variables dans le motif n'ont pas besoin de correspondre aux noms des champs de la struct. Cependant, il est courant de faire correspondre les noms des variables aux noms des champs pour faciliter la mémoire de savoir quelles variables proviennent de quels champs. En raison de cette utilisation courante, et parce que l'écriture let Point { x: x, y: y } = p; contient beaucoup de duplication, Rust a une forme abrégée pour les motifs qui correspondent aux champs de struct : vous n'avez qu'à lister le nom du champ de struct, et les variables créées à partir du motif auront les mêmes noms. La Liste 18-13 se comporte de la même manière que le code de la Liste 18-12, mais les variables créées dans le motif let sont x et y au lieu de a et b.

Nom de fichier : src/main.rs

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x, y } = p;
    assert_eq!(0, x);
    assert_eq!(7, y);
}

Liste 18-13 : Déconstruction des champs de struct en utilisant la forme abrégée des champs de struct

Ce code crée les variables x et y qui correspondent aux champs x et y de la variable p. Le résultat est que les variables x et y contiennent les valeurs de la struct p.

Nous pouvons également déconstruire avec des valeurs littérales en tant que partie du motif de struct plutôt que de créer des variables pour tous les champs. Cela nous permet de tester certains des champs pour des valeurs particulières tout en créant des variables pour déconstruire les autres champs.

Dans la Liste 18-14, nous avons une expression match qui sépare les valeurs Point en trois cas : les points qui se trouvent directement sur l'axe x (ce qui est vrai lorsque y = 0), sur l'axe y (x = 0), ou sur aucun des deux axes.

Nom de fichier : src/main.rs

fn main() {
    let p = Point { x: 0, y: 7 };

    match p {
        Point { x, y: 0 } => println!("On the x axis at {x}"),
        Point { x: 0, y } => println!("On the y axis at {y}"),
        Point { x, y } => {
            println!("On neither axis: ({x}, {y})");
        }
    }
}

Liste 18-14 : Déconstruction et correspondance de valeurs littérales dans un motif

Le premier bras correspondra à tout point qui se trouve sur l'axe x en spécifiant que le champ y correspond si sa valeur correspond au littéral 0. Le motif crée toujours une variable x que nous pouvons utiliser dans le code de ce bras.

De manière similaire, le second bras correspond à tout point sur l'axe y en spécifiant que le champ x correspond si sa valeur est 0 et crée une variable y pour la valeur du champ y. Le troisième bras ne spécifie aucun littéral, donc il correspond à tout autre Point et crée des variables pour les deux champs x et y.

Dans cet exemple, la valeur p correspond au second bras en raison de x qui contient un 0, donc ce code imprimera On the y axis at 7.

Rappelez-vous qu'une expression match arrête de vérifier les bras une fois qu'elle a trouvé le premier motif correspondant, donc même si Point { x: 0, y: 0} est sur l'axe x et l'axe y, ce code n'imprimera que On the x axis at 0.

Déconstruction d'enums

Nous avons déjà déconstruit des enums dans ce livre (par exemple, dans la Liste 6-5), mais nous n'avons pas encore explicitement discuté que le motif pour déconstruire un enum correspond à la manière dont les données stockées dans l'enum sont définies. Par exemple, dans la Liste 18-15, nous utilisons l'enum Message de la Liste 6-2 et écrivons une expression match avec des motifs qui déconstruiront chaque valeur interne.

Nom de fichier : src/main.rs

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {
  1 let msg = Message::ChangeColor(0, 160, 255);

    match msg {
      2 Message::Quit => {
            println!(
                "The Quit variant has no data to destructure."
            );
        }
      3 Message::Move { x, y } => {
            println!(
                "Move in the x dir {x}, in the y dir {y}"
            );
        }
      4 Message::Write(text) => {
            println!("Text message: {text}");
        }
      5 Message::ChangeColor(r, g, b) => println!(
            "Change color to red {r}, green {g}, and blue {b}"
        ),
    }
}

Liste 18-15 : Déconstruction des variantes d'enum qui contiennent différents types de valeurs

Ce code imprimera Change color to red 0, green 160, and blue 255. Essayez de modifier la valeur de msg [1] pour voir le code des autres bras s'exécuter.

Pour les variantes d'enum sans données, comme Message::Quit [2], nous ne pouvons pas déconstruire la valeur plus loin. Nous ne pouvons que correspondre à la valeur littérale Message::Quit, et aucun variable n'est dans ce motif.

Pour les variantes d'enum ressemblant à des structs, telles que Message::Move [3], nous pouvons utiliser un motif similaire à celui que nous spécifions pour correspondre à des structs. Après le nom de la variante, nous plaçons des accolades et ensuite listons les champs avec des variables pour que nous séparions les parties à utiliser dans le code de ce bras. Ici, nous utilisons la forme abrégée comme nous l'avons fait dans la Liste 18-13.

Pour les variantes d'enum ressemblant à des tuples, comme Message::Write qui contient un tuple avec un élément [4] et Message::ChangeColor qui contient un tuple avec trois éléments [5], le motif est similaire à celui que nous spécifions pour correspondre à des tuples. Le nombre de variables dans le motif doit correspondre au nombre d'éléments dans la variante que nous sommes en train de correspondre.

Déconstruction de structs et d'enums imbriqués

Jusqu'à présent, nos exemples ont tous été des correspondances de structs ou d'enums à une seule profondeur, mais la correspondance peut également fonctionner sur des éléments imbriqués! Par exemple, nous pouvons refactoriser le code de la Liste 18-15 pour prendre en charge les couleurs RGB et HSV dans le message ChangeColor, comme montré dans la Liste 18-16.

Nom de fichier : src/main.rs

enum Color {
    Rgb(i32, i32, i32),
    Hsv(i32, i32, i32),
}

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(Color),
}

fn main() {
    let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));

    match msg {
        Message::ChangeColor(Color::Rgb(r, g, b)) => println!(
            "Change color to red {r}, green {g}, and blue {b}"
        ),
        Message::ChangeColor(Color::Hsv(h, s, v)) => println!(
            "Change color to hue {h}, saturation {s}, value {v}"
        ),
        _ => (),
    }
}

Liste 18-16 : Correspondance sur des enums imbriqués

Le motif du premier bras dans l'expression match correspond à une variante d'enum Message::ChangeColor qui contient une variante Color::Rgb ; puis le motif se lie aux trois valeurs i32 internes. Le motif du second bras correspond également à une variante d'enum Message::ChangeColor, mais l'enum interne correspond à Color::Hsv à la place. Nous pouvons spécifier ces conditions complexes dans une seule expression match, même si deux enums sont impliqués.

Déconstruction de structs et de tuples

Nous pouvons mélanger, combiner et imbriquer des motifs de déconstruction de manière encore plus complexe. L'exemple suivant montre une déconstruction complexe où nous imbriquons des structs et des tuples à l'intérieur d'un tuple et décomposons toutes les valeurs primitives :

let ((feet, inches), Point { x, y }) =
    ((3, 10), Point { x: 3, y: -10 });

Ce code nous permet de séparer des types complexes en leurs parties constitutives afin que nous puissions utiliser séparément les valeurs qui nous intéressent.

La déconstruction avec des motifs est un moyen pratique d'utiliser des parties de valeurs, telles que les valeurs de chaque champ dans une struct, séparément les unes des autres.

Ignorer des valeurs dans un motif

Vous avez vu qu'il est parfois utile d'ignorer des valeurs dans un motif, par exemple dans le dernier bras d'un match, pour avoir un cas général qui ne fait en réalité rien mais qui prend en compte toutes les valeurs possibles restantes. Il existe plusieurs façons d'ignorer des valeurs entières ou des parties de valeurs dans un motif : en utilisant le motif _ (que vous avez déjà vu), en utilisant le motif _ à l'intérieur d'un autre motif, en utilisant un nom qui commence par un underscore, ou en utilisant .. pour ignorer les parties restantes d'une valeur. Explorerons comment et pourquoi utiliser chacun de ces motifs.

Une valeur entière avec _

Nous avons utilisé le tiret bas comme motif joker qui correspondra à n'importe quelle valeur mais ne se lira pas à la valeur. Cela est particulièrement utile comme dernier bras dans une expression match, mais nous pouvons également l'utiliser dans n'importe quel motif, y compris les paramètres de fonction, comme montré dans la Liste 18-17.

Nom de fichier : src/main.rs

fn foo(_: i32, y: i32) {
    println!("This code only uses the y parameter: {y}");
}

fn main() {
    foo(3, 4);
}

Liste 18-17 : Utilisation de _ dans une signature de fonction

Ce code ignorera complètement la valeur 3 passée en tant que premier argument et imprimera This code only uses the y parameter: 4.

Dans la plupart des cas, lorsque vous n'avez plus besoin d'un paramètre de fonction particulier, vous modifieriez la signature pour qu'elle n'inclue pas le paramètre non utilisé. Ignorer un paramètre de fonction peut être particulièrement utile dans des cas où, par exemple, vous implémentez un trait et que vous avez besoin d'un certain type de signature, mais que le corps de la fonction dans votre implémentation n'a pas besoin d'un des paramètres. Vous évitez ainsi de recevoir un avertissement du compilateur concernant les paramètres de fonction non utilisés, comme vous le feriez si vous utilisiez un nom à la place.

Parties d'une valeur avec un _ imbriqué

Nous pouvons également utiliser _ à l'intérieur d'un autre motif pour ignorer seulement une partie d'une valeur, par exemple, lorsque nous voulons tester seulement une partie d'une valeur mais n'avons pas besoin des autres parties dans le code correspondant que nous voulons exécuter. La Liste 18-18 montre le code responsable de la gestion de la valeur d'un paramètre. Les exigences commerciales sont que l'utilisateur ne doit pas être autorisé à écraser une personnalisation existante d'un paramètre, mais peut annuler le paramètre et lui donner une valeur s'il n'est pas actuellement défini.

Nom de fichier : src/main.rs

let mut setting_value = Some(5);
let new_setting_value = Some(10);

match (setting_value, new_setting_value) {
    (Some(_), Some(_)) => {
        println!("Can't overwrite an existing customized value");
    }
    _ => {
        setting_value = new_setting_value;
    }
}

println!("setting is {:?}", setting_value);

Liste 18-18 : Utilisation d'un tiret bas à l'intérieur de motifs qui correspondent à des variantes Some lorsque nous n'avons pas besoin d'utiliser la valeur à l'intérieur de Some

Ce code imprimera Can't overwrite an existing customized value puis setting is Some(5). Dans le premier bras de correspondance, nous n'avons pas besoin de correspondre ou d'utiliser les valeurs à l'intérieur des deux variantes Some, mais nous devons tester le cas où setting_value et new_setting_value sont la variante Some. Dans ce cas, nous imprimons la raison pour ne pas modifier setting_value, et elle ne sera pas modifiée.

Dans tous les autres cas (si setting_value ou new_setting_value est None) exprimé par le motif _ dans le second bras, nous voulons autoriser new_setting_value à devenir setting_value.

Nous pouvons également utiliser des tirets bas en plusieurs endroits à l'intérieur d'un motif pour ignorer des valeurs particulières. La Liste 18-19 montre un exemple d'ignorance de la deuxième et quatrième valeurs dans un tuple de cinq éléments.

Nom de fichier : src/main.rs

let numbers = (2, 4, 8, 16, 32);

match numbers {
    (first, _, third, _, fifth) => {
        println!("Some numbers: {first}, {third}, {fifth}");
    }
}

Liste 18-19 : Ignorer plusieurs parties d'un tuple

Ce code imprimera Some numbers: 2, 8, 32, et les valeurs 4 et 16 seront ignorées.

Une variable inutilisée en commençant son nom par _

Si vous créez une variable mais ne l'utilisez nulle part, Rust vous affichera généralement un avertissement car une variable inutilisée peut être un bogue. Cependant, parfois il est utile de pouvoir créer une variable que vous n'utiliserez pas encore, par exemple lorsque vous protégeez ou que vous commencez un projet. Dans cette situation, vous pouvez dire à Rust de ne pas vous avertir concernant la variable inutilisée en commençant le nom de la variable par un underscore. Dans la Liste 18-20, nous créons deux variables inutilisées, mais lorsque nous compilons ce code, nous devrions seulement recevoir un avertissement concernant l'une d'entre elles.

Nom de fichier : src/main.rs

fn main() {
    let _x = 5;
    let y = 10;
}

Liste 18-20 : Commencer le nom d'une variable par un underscore pour éviter les avertissements concernant les variables inutilisées

Ici, nous recevons un avertissement concernant l'utilisation de la variable y, mais nous ne recevons pas d'avertissement concernant l'utilisation de _x.

Notez qu'il existe une différence subtile entre l'utilisation uniquement de _ et l'utilisation d'un nom commençant par un underscore. La syntaxe _x lie toujours la valeur à la variable, tandis que _ ne lie pas du tout. Pour montrer un cas où cette distinction est importante, la Liste 18-21 nous donnera une erreur.

Nom de fichier : src/main.rs

let s = Some(String::from("Hello!"));

if let Some(_s) = s {
    println!("found a string");
}

println!("{:?}", s);

Liste 18-21 : Une variable inutilisée commençant par un underscore lie toujours la valeur, ce qui peut prendre la propriété de la valeur.

Nous recevrons une erreur car la valeur s sera toujours déplacée dans _s, ce qui nous empêche d'utiliser s à nouveau. Cependant, l'utilisation du underscore seul ne lie jamais à la valeur. La Liste 18-22 se compilera sans erreurs car s n'est pas déplacé dans _.

Nom de fichier : src/main.rs

let s = Some(String::from("Hello!"));

if let Some(_) = s {
    println!("found a string");
}

println!("{:?}", s);

Liste 18-22 : L'utilisation d'un underscore ne lie pas la valeur.

Ce code fonctionne parfaitement car nous ne liions jamais s à rien ; elle n'est pas déplacée.

Parties restantes d'une valeur avec..

Avec des valeurs qui ont de nombreuses parties, nous pouvons utiliser la syntaxe .. pour utiliser des parties spécifiques et ignorer le reste, évitant ainsi d'avoir à énumérer des underscores pour chaque valeur ignorée. Le motif .. ignore toutes les parties d'une valeur que nous n'avons pas explicitement correspondues dans le reste du motif. Dans la Liste 18-23, nous avons une structure Point qui stocke une coordonnée dans l'espace tridimensionnel. Dans l'expression match, nous voulons opérer uniquement sur la coordonnée x et ignorer les valeurs dans les champs y et z.

Nom de fichier : src/main.rs

struct Point {
    x: i32,
    y: i32,
    z: i32,
}

let origin = Point { x: 0, y: 0, z: 0 };

match origin {
    Point { x,.. } => println!("x is {x}"),
}

Liste 18-23 : Ignorer tous les champs d'un Point sauf x en utilisant ..

Nous listons la valeur x puis nous incluons simplement le motif ... Cela est plus rapide que d'avoir à énumérer y: _ et z: _, en particulier lorsque nous travaillons avec des structs qui ont de nombreux champs dans des situations où seulement un ou deux champs sont pertinents.

La syntaxe .. s'étendra à autant de valeurs qu'il en faut. La Liste 18-24 montre comment utiliser .. avec un tuple.

Nom de fichier : src/main.rs

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (first,.., last) => {
            println!("Some numbers: {first}, {last}");
        }
    }
}

Liste 18-24 : Correspondre uniquement aux premières et dernières valeurs d'un tuple et ignorer toutes les autres valeurs

Dans ce code, les premières et dernières valeurs sont correspondues avec first et last. Le .. correspondra et ignorera tout le reste.

Cependant, l'utilisation de .. doit être sans ambiguïté. Si il n'est pas clair quelles valeurs sont destinées à être correspondues et lesquelles doivent être ignorées, Rust nous donnera une erreur. La Liste 18-25 montre un exemple d'utilisation ambiguë de .., donc elle ne se compilera pas.

Nom de fichier : src/main.rs

fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
        (.., second,..) => {
            println!("Some numbers: {second}");
        },
    }
}

Liste 18-25 : Tentative d'utilisation ambiguë de ..

Lorsque nous compilons cet exemple, nous obtenons cette erreur :

error: `..` can only be used once per tuple pattern
 --> src/main.rs:5:22
  |
5 |         (.., second,..) => {
  |          --          ^^ can only be used once per tuple pattern
  |          |
  |          previously used here

Il est impossible pour Rust de déterminer combien de valeurs dans le tuple doivent être ignorées avant de correspondre une valeur avec second et puis combien de valeurs supplémentaires doivent être ignorées ensuite. Ce code pourrait signifier que nous voulons ignorer 2, lier second à 4 et puis ignorer 8, 16 et 32 ; ou que nous voulons ignorer 2 et 4, lier second à 8 et puis ignorer 16 et 32 ; etc. Le nom de variable second n'a rien de spécial pour Rust, donc nous obtenons une erreur du compilateur car utiliser .. en deux endroits comme ceci est ambigu.

Conditions supplémentaires avec des gardes de correspondance

Une garde de correspondance est une condition if supplémentaire, spécifiée après le motif dans un bras de match, qui doit également correspondre pour que ce bras soit choisi. Les gardes de correspondance sont utiles pour exprimer des idées plus complexes que ne le permet un motif seul.

La condition peut utiliser les variables créées dans le motif. La Liste 18-26 montre un match où le premier bras a le motif Some(x) et également une garde de correspondance if x % 2 == 0 (qui sera true si le nombre est pair).

Nom de fichier : src/main.rs

let num = Some(4);

match num {
    Some(x) if x % 2 == 0 => println!("The number {x} is even"),
    Some(x) => println!("The number {x} is odd"),
    None => (),
}

Liste 18-26 : Ajout d'une garde de correspondance à un motif

Cet exemple imprimera The number 4 is even. Lorsque num est comparé au motif dans le premier bras, il correspond car Some(4) correspond à Some(x). Ensuite, la garde de correspondance vérifie si le reste de la division de x par 2 est égal à 0, et puisque c'est le cas, le premier bras est sélectionné.

Si num avait été Some(5) au lieu de Some(4), la garde de correspondance dans le premier bras serait restée false car le reste de la division de 5 par 2 est 1, ce qui n'est pas égal à 0. Rust passerait alors au second bras, qui correspondrait car le second bras n'a pas de garde de correspondance et correspond donc à n'importe quelle variante Some.

Il n'est pas possible d'exprimer la condition if x % 2 == 0 à l'intérieur d'un motif, donc la garde de correspondance nous permet d'exprimer cette logique. Le désavantage de cette expressivité supplémentaire est que le compilateur n'essaie pas de vérifier l'exhaustivité lorsqu'il y a des expressions de garde de correspondance.

Dans la Liste 18-11, nous avons mentionné que nous pouvions utiliser des gardes de correspondance pour résoudre notre problème d'ombrage de motif. Rappelez-vous que nous avons créé une nouvelle variable à l'intérieur du motif dans l'expression match au lieu d'utiliser la variable en dehors du match. Cette nouvelle variable signifiait que nous ne pouvions pas tester la valeur de la variable externe. La Liste 18-27 montre comment nous pouvons utiliser une garde de correspondance pour résoudre ce problème.

Nom de fichier : src/main.rs

fn main() {
    let x = Some(5);
    let y = 10;

    match x {
        Some(50) => println!("Got 50"),
        Some(n) if n == y => println!("Matched, n = {n}"),
        _ => println!("Default case, x = {:?}", x),
    }

    println!("at the end: x = {:?}, y = {y}", x);
}

Liste 18-27 : Utilisation d'une garde de correspondance pour tester l'égalité avec une variable externe

Ce code imprimera maintenant Default case, x = Some(5). Le motif dans le second bras de correspondance n'introduit pas une nouvelle variable y qui ombrerait la variable externe y, ce qui signifie que nous pouvons utiliser la variable externe y dans la garde de correspondance. Au lieu de spécifier le motif comme Some(y), qui aurait ombré la variable externe y, nous spécifions Some(n). Cela crée une nouvelle variable n qui n'ombre rien car il n'y a pas de variable n en dehors du match.

La garde de correspondance if n == y n'est pas un motif et ne introduit donc pas de nouvelles variables. Cette y est bien la variable externe y plutôt qu'une nouvelle y ombrée, et nous pouvons chercher une valeur qui a la même valeur que la variable externe y en comparant n à y.

Vous pouvez également utiliser l'opérateur ou | dans une garde de correspondance pour spécifier plusieurs motifs ; la condition de la garde de correspondance s'appliquera à tous les motifs. La Liste 18-28 montre la précédence lorsqu'on combine un motif qui utilise | avec une garde de correspondance. La partie importante de cet exemple est que la garde de correspondance if y s'applique à 4, 5 et 6, même si cela peut sembler que if y ne s'applique qu'à 6.

Nom de fichier : src/main.rs

let x = 4;
let y = false;

match x {
    4 | 5 | 6 if y => println!("yes"),
    _ => println!("no"),
}

Liste 18-28 : Combinaison de plusieurs motifs avec une garde de correspondance

La condition de correspondance indique que le bras ne correspond que si la valeur de x est égale à 4, 5 ou 6 et si y est true. Lorsque ce code s'exécute, le motif du premier bras correspond car x est 4, mais la garde de correspondance if y est false, donc le premier bras n'est pas choisi. Le code passe au second bras, qui correspond effectivement, et ce programme imprimera no. La raison est que la condition if s'applique au motif entier 4 | 5 | 6, pas seulement à la dernière valeur 6. En d'autres termes, la précédence d'une garde de correspondance par rapport à un motif se comporte comme ceci :

(4 | 5 | 6) if y =>...

plutôt que comme ceci :

4 | 5 | (6 if y) =>...

Après avoir exécuté le code, le comportement de précédence est évident : si la garde de correspondance était appliquée seulement à la valeur finale de la liste de valeurs spécifiée à l'aide de l'opérateur |, le bras aurait correspondu et le programme aurait imprimé yes.

@ Liaisons

L'opérateur au @ nous permet de créer une variable qui stocke une valeur en même temps que nous testons cette valeur pour une correspondance de motif. Dans la Liste 18-29, nous voulons tester qu'un champ id de type Message::Hello est dans la plage 3..=7. Nous voulons également lier la valeur à la variable id_variable afin que nous puissions l'utiliser dans le code associé au bras. Nous pourrions nommer cette variable id, comme le champ, mais pour cet exemple, nous utiliserons un nom différent.

Nom de fichier : src/main.rs

enum Message {
    Hello { id: i32 },
}

let msg = Message::Hello { id: 5 };

match msg {
    Message::Hello {
        id: id_variable @ 3..=7,
    } => println!("Found an id in range: {id_variable}"),
    Message::Hello { id: 10..=12 } => {
        println!("Found an id in another range")
    }
    Message::Hello { id } => println!("Some other id: {id}"),
}

Liste 18-29 : Utilisation de @ pour lier une valeur dans un motif tout en la testant

Cet exemple imprimera Found an id in range: 5. En spécifiant id_variable @ avant la plage 3..=7, nous capturons la valeur qui a correspondu à la plage tout en testant que la valeur correspondait au motif de plage.

Dans le second bras, où nous n'avons spécifié que la plage dans le motif, le code associé au bras n'a pas de variable qui contient la valeur réelle du champ id. La valeur du champ id aurait pu être 10, 11 ou 12, mais le code associé à ce motif ne sait pas laquelle. Le code du motif n'est pas capable d'utiliser la valeur du champ id car nous n'avons pas enregistré la valeur de id dans une variable.

Dans le dernier bras, où nous avons spécifié une variable sans plage, nous avons bien la valeur disponible pour l'utiliser dans le code du bras dans une variable nommée id. La raison est que nous avons utilisé la syntaxe raccourcie de champ de structure. Mais nous n'avons pas appliqué de test à la valeur dans le champ id dans ce bras, comme nous l'avons fait avec les deux premiers bras : n'importe quelle valeur correspondrait à ce motif.

L'utilisation de @ nous permet de tester une valeur et de la sauvegarder dans une variable à l'intérieur d'un seul motif.

Sommaire

Félicitations ! Vous avez terminé le laboratoire de syntaxe des motifs. Vous pouvez pratiquer d'autres laboratoires sur LabEx pour améliorer vos compétences.