La syntaxe des motifs

Tout au long de ce livre, vous avez rencontré de nombreux types de motifs. Dans cette section, nous allons rassembler toutes les syntaxes valides des motifs et examiner les raisons pour lesquelles vous devriez utiliser chacune d'entre elles.

Correspondre aux littéraux

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

fn main() {
    let x = 1;

    match x {
        1 => println!("un"),
        2 => println!("deux"),
        3 => println!("trois"),
        _ => println!("n'importe quoi"),
    }
}

Ce code affiche un car la valeur dans x est 1. Cette syntaxe est très utile lorsque vous souhaitez que votre code fasse quelque chose s'il obtient une valeur précise.

Correspondre à 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 fois dans le livre. Cependant, il subsiste un problème lorsque vous utilisez les variables nommées dans les expressions match. Comme match débute une nouvelle portée, les variables utilisées comme faisant partie du motif de la construction match vont masquer celles ayant le même nom et provenant de l'extérieur de la construction match, comme c'est le cas avec toutes les variables. Dans l'encart 18-11, nous déclarons une variable x avec la valeur Some(5) et une variable y avec la valeur 10. Nous créons alors une expression match sur la valeur x. Observez les motifs sur les branches du match et du println! à la fin, et essayez de deviner ce qui sera écrit avant d'exécuter ce code ou de lire la suite.

Fichier : src/main.rs

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

    match x {
        Some(50) => println!("On a 50"),
        Some(y) => println!("Correspondance, y = {:?}", y),
        _ => println!("Cas par défaut, x = {:?}", x),
    }

    println!("A la fin : x = {:?}, y = {:?}", x, y);
}

Encart 18-11 : une expression match avec une branche qui crée une variable masquée y

Voyons ce qui se passe lorsque l'expression match est utilisée. Le motif présent dans la première branche du match ne correspond pas à la valeur actuelle de x, donc le code passe à la branche suivante.

Le motif dans la deuxième branche du match ajoute une nouvelle variable y qui va correspondre à n'importe quelle valeur logée dans une valeur Some. Comme nous sommes dans une nouvelle portée à l'intérieur de l'expression match, c'est une nouvelle variable y, et pas le y que nous avons déclaré au début avec la valeur 10. Cette nouvelle correspondance y va correspondre à n'importe quelle valeur à l'intérieur d'un Some, ce qui est la situation présente actuellement dans x. Ainsi, ce nouveau y correspondra à la valeur interne du Some présent dans x. Cette valeur est 5, donc l'expression de cette branche s'exécute et affiche Correspondance, y = 5.

En supposant maintenant que x ait la valeur None plutôt que Some(5), les motifs présents dans les deux premières branches ne correspondront pas, donc la valeur qui correspondra sera celle avec le tiret du bas. Comme nous n'avons pas introduit de nouvelle variable x dans la branche du motif, le x de l'expression associée désigne toujours la variable x en dehors et qui n'a pas été masquée. Le match va donc afficher Cas par défaut, x = None.

Lorsque l'expression match est terminée, sa portée se termine également, et avec elle la portée de la variable interne y. Le dernier println! affiche donc A la fin : x = Some(5), y = 10.

Pour créer une expression match qui compare les valeurs de la variable externe x avec y, plutôt que d'utiliser une variable masquée, nous aurions besoin d'utiliser à la place un contrôle de correspondance. Nous verrons les contrôles de correspondance dans une des sections suivantes.

Plusieurs motifs

Dans les expressions match, vous pouvez faire correspondre une même branche à plusieurs motifs en utilisant la syntaxe |, qui signifie ou. Par exemple, dans le code suivant appliquant un match sur la valeur de x, la première des branches possède une option ou, ce qui signifie que si la valeur de x correspond à l'un ou l'autre des motifs de cette branche, le code associé sera exécuté :

fn main() {
    let x = 1;

    match x {
        1 | 2 => println!("un ou deux"),
        3 => println!("trois"),
        _ => println!("quelque chose d'autre"),
    }
}

Ce code va afficher un ou deux.

Faire correspondre un intervalle de valeurs avec ..=

La syntaxe ..= nous permet de faire correspondre un intervalle inclusif de valeurs. Dans le code suivant, lorsqu'un motif correspond à une des valeurs présentes dans l'intervalle, cette branche va s'exécuter :

fn main() {
    let x = 5;

    match x {
        1..=5 => println!("de un à cinq"),
        _ => println!("quelque chose d'autre"),
    }
}

Si x vaut 1, 2, 3, 4 ou 5, la première branche va correspondre. Cette syntaxe est plus pratique à utiliser que d'avoir à utiliser l'opérateur | pour exprimer la même idée ; à la place de 1..=5 nous aurions dû écrire 1 | 2 | 3 | 4 | 5 si nous avions utilisé |. Renseigner un intervalle est bien plus court, en particulier si nous souhaitons avoir une correspondance avec les valeurs comprises entre 1 et 1000 par exemple !

Les intervalles peuvent être des nombres ou des char (caractères), car le compilateur vérifie que l'intervalle n'est pas vide au moment de la compilation et les seuls types pour lesquels Rust peut dire si un intervalle est vide ou non sont ceux constitués de nombres ou de char.

Voici un exemple d'utilisation d'intervalles de char :

fn main() {
    let x = 'c';

    match x {
        'a'..='j' => println!("lettre ASCII du début"),
        'k'..='z' => println!("lettre ASCII de la fin"),
        _ => println!("autre chose"),
    }
}

Rust peut nous dire que c est dans le premier intervalle du premier motif et afficher lettre ASCII du début.

Destructurer pour séparer les valeurs

Nous pouvons aussi utiliser les motifs pour destructurer les structures, les énumérations, et les tuples pour utiliser différentes parties de ces valeurs. Passons en revue chacun des cas.

Destructurer les structures

L'encart 18-12 montre une structure Point avec deux champs, x et y, que nous pouvons séparer en utilisant un motif avec une instruction let.

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

Encart 18-12 : déstructuration des champs d'une structure dans des variables séparées

Ce code crée les variables a et b qui correspondent aux valeurs des champs x et y de la structure p. Cet exemple montre que les noms des variables du motif n'ont pas à correspondre aux noms des champs de la structure. Mais il est courant de vouloir faire correspondre le nom des variables avec le nom des champs pour se rappeler plus facilement quelle variable provient de quel champ.

Comme faire correspondre les noms des variables avec ceux des champs est une pratique courante et qu'écrire let Point { x: x, y: y } = p; est inutilement redondant, il existe un raccourci pour les motifs qui correspondent aux champs des structures : il vous suffit de lister simplement le nom des champs de la structure pour que les variables créées à partir du motif aient les mêmes noms. L'encart 18-12 montre du code qui se comporte de la même manière que le code de l'encart 18-12, mais dans lequel les variables créées dans le motif du let sont x et y au lieu de a et b.

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

Encart 18-13 : déstructuration des champs d'une structure en utilisant le raccourci pour les champs des structures

Ce code crée les variables x et y qui correspondent aux champs x et y de la variable p. Il en résulte que les variables x et y contiennent les valeurs correspondantes de la structure p.

Nous pouvons aussi destructurer en utilisant des valeurs littérales faisant partie du motif de la structure plutôt que d'avoir à créer les variables pour tous les champs. Ceci nous permet de tester que certains champs possèdent des valeurs particulières tout en créant des variables pour destructurer les autres champs.

L'encart 18-14 montre une expression match qui sépare les valeurs Point en trois catégories : les points qui sont sur l'axe x (ce qui est vrai lorsque y = 0), ceux sur l'axe y (x = 0) et ceux qui ne sont sur aucun de ces deux axes.

Fichier : src/main.rs

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

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

    match p {
        Point { x, y: 0 } => println!("Sur l'axe x à la position {}", x),
        Point { x: 0, y } => println!("Sur l'axe y à la position {}", y),
        Point { x, y } => println!("Sur aucun des axes : ({}, {})", x, y),
    }
}

Encart 18-14 : déstructurer et faire correspondre des valeurs littérales grâce à un seul motif

La première branche va correspondre avec tous les points qui se trouvent sur l'axe x en précisant que le champ y correspond au littéral 0. Le motif va systématiquement créer une variable x que nous pourrons utiliser dans le code de cette branche.

De la même manière, la deuxième branche correspondra avec tous les points sur l'axe y en précisant que le champ x correspondra uniquement si sa valeur est 0 et créera une variable y pour la valeur du champ y. La troisième branche n'a pas besoin d'un littéral en particulier, donc elle correspondra à n'importe quel autre Point et créera les variables pour les champs x et y.

Dans cet exemple, la valeur p correspond avec la deuxième branche car son x vaut 0, donc ce code va afficher Sur l'axe y à la position 7.

Destructurer une énumération

Nous avons déjà destructuré des énumérations précédemment dans ce livre, par exemple lorsque nous avions destructuré Option<i32> dans l'encart 6-5 du chapitre 6. Un détail que nous n'avions pas précisé explicitement était que le motif pour destructurer une énumération doit correspondre à la façon dont sont définies les données dans l'énumération. Par exemple, dans l'encart 18-15 nous utilisons l'énumération Message de l'encart 6-2 et nous ajoutons un match avec des motifs qui devraient destructurer chaque valeur interne.

Fichier : src/main.rs

enum Message {
    Quitter,
    Deplacer { x: i32, y: i32 },
    Ecrire(String),
    ChangerCouleur(i32, i32, i32),
}

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

    match msg {
        Message::Quitter => {
            println!("La variante Quitter n'a pas de données à déstructurer.")
        }
        Message::Deplacer { x, y } => {
            println!(
                "Déplacement de {} sur l'axe x et de {} sur l'axe y",
                x, y
            );
        }
        Message::Ecrire(text) => println!("Message textuel : {}", text),
        Message::ChangerCouleur(r, g, b) => println!(
            "Changement des taux de rouge à {}, de vert à {} et de bleu à {}",
            r, g, b
        ),
    }
}

Encart 18-15 : déstructuration des variantes d'une énumération qui stocke différents types de valeurs

Ce code va afficher Changement des taux de rouge à 0, de vert à 160 et de bleu à 255. Essayez de changer la valeur de message pour voir le code qu'exécute les autres branches.

Pour les variantes d'énumération sans aucune donnée, telle que Message::Quitter, nous ne pouvons pas destructurer de valeurs. Nous pouvons uniquement correspondre à la valeur littérale Message::Quitter et il n'y a pas de variable dans ce motif.

Pour les variantes d'énumération qui ressemblent aux structures, comme Message::Deplacer, nous pouvons utiliser un motif similaire aux motifs que nous utilisons pour correspondre aux structures. Après le nom de la variante, nous utilisons des accolades puis nous listons les champs avec des variables afin de diviser les éléments à utiliser dans le code de cette branche. Ici nous utilisons la forme raccourcie comme nous l'avons fait à l'encart 18-13.

Pour les variantes d'énumérations qui ressemblent à des tuples, telles que Message::Ecrire qui stocke un tuple avec un seul élément, ou Message::ChangerCouleur qui stocke un tuple avec trois éléments, le motif est semblable à celui que nous renseignons pour correspondre aux tuples. Le nombre de variables dans le motif doit correspondre au nombre d'éléments dans la variante qui correspond.

Destructurer des structures et des énumérations imbriquées

Jusqu'à présent, tous nos exemples avaient des correspondances avec des structures ou des énumérations qui n'avaient qu'un seul niveau de profondeur. Les correspondances fonctionnent aussi sur les éléments imbriqués !

Par exemple, nous pouvons remanier le code de l'encart 18-15 pour pouvoir utiliser des couleurs RVB et TSV dans le message ChangerCouleur, comme dans l'encart 18-16.

enum Couleur {
    Rvb(i32, i32, i32),
    Tsv(i32, i32, i32),
}

enum Message {
    Quitter,
    Deplacer { x: i32, y: i32 },
    Ecrire(String),
    ChangerCouleur(Couleur),
}

fn main() {
    let msg = Message::ChangerCouleur(Couleur::Tsv(0, 160, 255));

    match msg {
        Message::ChangerCouleur(Couleur::Rvb(r, v, b)) => println!(
            "Changement des taux de rouge à {}, de vert à {} et de bleu à {}",
            r, v, b
        ),
        Message::ChangerCouleur(Couleur::Tsv(t, s, v)) => println!(
            "Changement des taux de teinte à {}, de saturation à {} et de valeur à {}",
            t, s, v
        ),
        _ => (),
    }
}

Encart 18-16 : correspondance avec des énumérations imbriquées

Le motif de la première branche dans l'expression match correspond à la variante d'énumération Message::ChangerCouleur qui contient une variante Couleur::Rvb ; ensuite le motif fait correspondre des variables aux trois valeurs i32 que cette dernière contient. Le motif de la seconde branche correspond aussi à une variante de l'énumération de Message::ChangerCouleur, mais la valeur interne correspond plutôt à la variante Couleur::Tsv. Nous pouvons renseigner ces conditions complexes dans une seule expression match, bien que deux énumérations différentes soient impliquées.

Destructurer des structures et des tuples

Nous pouvons mélanger les correspondances et les motifs pour déstructurer des éléments imbriqués de manière bien plus complexe. L'exemple suivant montre une déstructuration complexe dans laquelle nous imbriquons des structures et des tuples à l'intérieur d'un tuple et nous y destructurons toutes les valeurs primitives :

fn main() {
    struct Point {
        x: i32,
        y: i32,
    }

    let ((pieds, pouces), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
}

Ce code nous permet de décomposer les parties qui composent des types complexes pour pouvoir utiliser séparément les valeurs qui nous intéressent.

La déstructuration avec les motifs est un moyen efficace d'utiliser des parties de valeurs, comme par exemple la valeur de chaque champ d'une structure, indépendamment les unes des autres.

Ignorer des valeurs dans un motif

Vous avez pu constater qu'il est parfois utile d'ignorer des valeurs dans un motif, comme celle dans la dernière branche d'un match, pour obtenir un joker qui ne fait rien mis à part qu'il représente toutes les autres valeurs possibles. Il existe plusieurs façons d'ignorer totalement ou en partie des valeurs dans un motif : en utilisant le motif _ (que vous avez déjà vu), le motif _ à l'intérieur d'un autre motif, un nom qui commence avec un tiret bas, ou enfin .. pour ignorer les parties restantes d'une valeur. Voyons comment et pourquoi utiliser ces différents motifs.

Ignorer complètement une valeur avec _

Nous avons utilisé le tiret bas (_) comme un motif joker qui correspondra avec n'importe quelle valeur mais ne l'assignera pas. Bien que le motif du tiret bas _ soit particulièrement utile dans la dernière branche d'une expression match, nous pouvons aussi l'utiliser dans n'importe quel motif, y compris dans les paramètres de fonctions, comme montré dans l'encart 18-17.

Fichier : src/main.rs

fn fonction(_: i32, y: i32) {
    println!("Ce code utilise uniquement le paramètre y : {}", y);
}

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

Encart 18-17 : utilisation d'un _ dans la signature d'une fonction

Ce code va complètement ignorer la valeur envoyée en premier argument, 3, et va afficher Ce code utilise uniquement le paramètre y : 4.

Dans la plupart des cas lorsque vous n'avez pas besoin d'un paramètre d'une fonction, vous pouvez changer la signature pour qu'elle n'inclut pas le paramètre non utilisé. Ignorer un paramètre de fonction peut être particulièrement utile dans certains cas, comme par exemple, lors de l'implémentation d'un trait lorsque 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. Le compilateur ne vous avertira plus que ces paramètres de fonction ne sont pas utilisés, ce qui serait le cas si vous utilisiez un nom à la place.

Ignorer des parties d'une valeur en utilisant un _ imbriqué

Nous pouvons aussi utiliser _ au sein d'un autre motif pour ignorer uniquement une partie d'une valeur, par exemple, si nous ne souhaitons tester qu'une seule partie d'une valeur mais que nous n'utilisons pas les autres parties dans le code que nous souhaitons exécuter. L'encart 18-18 montre du code qui s'occupe de gérer la valeur d'un réglage. Les règles métier sont que l'utilisateur ne doit pas pouvoir modifier un réglage existant mais peut annuler le réglage ou lui donner une valeur s'il n'en a pas encore.

fn main() {
    let mut valeur_du_reglage = Some(5);
    let nouvelle_valeur_du_reglage = Some(10);

    match (valeur_du_reglage, nouvelle_valeur_du_reglage) {
        (Some(_), Some(_)) => {
            println!("Vous ne pouvez pas écraser une valeur déjà existante");
        }
        _ => {
            valeur_du_reglage = nouvelle_valeur_du_reglage;
        }
    }

    println!("Le réglage vaut {:?}", valeur_du_reglage);
}

Encart 18-18 : utilisation d'un tiret bas dans des motifs qui correspondent avec des variantes Some lorsque nous n'avons pas besoin d'utiliser la valeur à l'intérieur du Some

Ce code va afficher Vous ne pouvez pas écraser une valeur déjà existante et ensuite Le réglage vaut Some(5). Dans la première branche, nous n'avons pas besoin de récupérer ou d'utiliser les valeurs à l'intérieur de chacune des variantes Some, mais nous avons besoin de tester les situations où valeur_du_reglage et nouvelle_valeur_du_reglage sont toutes deux des variantes Some. Dans ce cas, nous écrivons que nous n'allons pas changer valeur_du_reglage et elle ne changera pas.

Dans tous les autres cas (lorsque soit valeur_du_reglage, soit nouvelle_valeur_du_reglage vaut None) qui correspondront avec le motif _ de la seconde branche, nous voulons permettre à la valeur de nouvelle_valeur_du_reglage de remplacer celle de valeur_du_reglage.

Nous pouvons aussi utiliser les tirets bas à plusieurs endroits dans un même motif pour ignorer des valeurs précises. L'encart 18-19 montre un exemple qui ignore la deuxième et la quatrième valeur dans un tuple de cinq éléments.

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

    match nombres {
        (premier, _, troisieme, _, cinquieme) => {
            println!("Voici quelques nombres : {}, {}, {}", premier, troisieme, cinquieme)
        }
    }
}

Encart 18-19 : on ignore plusieurs éléments d'un tuple

Ce code va afficher Voici quelques nombres : 2, 8, 32 tandis que les valeurs 4 et 16 sont ignorées.

Ignorer une variable non utilisée en préfixant son nom avec un _

Si vous créez une variable mais que vous ne l'utilisez nulle part, Rust va lancer un avertissement car cela pourrait être un bogue. Mais parfois il est utile de créer une variable que vous n'utilisez pas encore, ce qui peut arriver lorsque vous créez un prototype ou un projet. Dans ce genre de situation, vous pouvez demander à Rust de ne pas vous avertir que la variable n'est pas utilisée en préfixant son nom avec un tiret bas. Dans l'encart 18-20, nous créons deux variables non utilisées, mais lorsque nous compilerons ce code, nous n'aurons d'avertissement que pour une seule d'entre elles.

Fichier : src/main.rs

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

Encart 18-20 : préfixer le nom d'une variable avec un tiret bas pour éviter d'avoir des avertissements signalant une variable non utilisée

Ici nous avons un avertissement qui nous prévient que nous n'utilisons pas la variable y, mais nous n'avons pas d'avertissement concernant la variable dont le nom est préfixé par un tiret bas.

Notez qu'il existe une différence subtile entre utiliser uniquement _ et préfixer un nom avec un tiret bas. La syntaxe _x continue à associer la valeur à une variable, alors que _ ne le fait pas du tout. Pour montrer un cas où cette différence est importante, l'encart 18-21 va nous donner une erreur.

fn main() {
    let s = Some(String::from("Salutations !"));

    if let Some(_s) = s {
        println!("j'ai trouvé une chaine de caractères");
    }

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

Encart 18-21 : une variable non utilisée préfixée par un tiret bas continue à assigner la valeur, ce qui pourrait entraîner une prise de possession de la valeur

Nous allons obtenir une erreur car la valeur s est toujours déplacée dans _s, ce qui nous empêche d'utiliser s ensuite. A l'inverse, l'utilisation du tiret bas tout seul n'assigne jamais la valeur à quelque chose. Par conséquent, l'encart 18-22 va se compiler sans aucune erreur car s n'est pas déplacé dans _.

fn main() {
    let s = Some(String::from("Salutations !"));

    if let Some(_) = s {
        println!("j'ai trouvé une chaine de caractères");
    }

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

Encart 18-22 : l'utilisation d'un tiret bas n'assigne pas la valeur

Ce code fonctionne correctement car nous n'assignons jamais s à quelque chose ; elle n'est jamais déplacée.

Ignorer les éléments restants d'une valeur avec ..

Avec les valeurs qui ont de nombreux éléments, nous pouvons utiliser la syntaxe .. pour n'utiliser que quelques éléments et ignorer les autres, ce qui évite d'avoir à faire une liste de tirets bas pour chacune des valeurs ignorées. Le motif .. ignore tous les éléments d'une valeur qui ne correspondent pas explicitement au reste du motif. Dans l'encart 18-23, nous avons une structure Point qui stocke des coordonnées dans un espace tridimensionnel. Dans l'expression match, nous souhaitons utiliser uniquement la coordonnée x et ignorer les valeurs des champs y et z.

fn main() {
    struct Point {
        x: i32,
        y: i32,
        z: i32,
    }

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

    match origine {
        Point { x, .. } => println!("x vaut {}", x),
    }
}

Encart 18-23 : on ignore tous les champs d'un Point à l'exception de x en utilisant ..

Nous ajoutons la valeur x puis nous insérons simplement le motif ... C'est plus rapide que d'avoir à ajouter y: _ et z: _, en particulier lorsque nous travaillons avec des structures qui ont beaucoup de champs alors qu'un seul champ ou deux nous intéressent.

La syntaxe .. va s'étendre à toutes les valeurs qu'elle devra couvrir. L'encart 18-24 montre comment utiliser .. avec un tuple.

Fichier : src/main.rs

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

    match nombres {
        (premier, .., dernier) => {
            println!("Voici quelques nombres : {}, {}", premier, dernier);
        }
    }
}

Encart 18-24 : on correspond uniquement avec la première et la dernière valeur d'un tuple en ignorant toutes les autres valeurs

Dans ce code, la première et la dernière valeur correspondent à premier et dernier. Le .. va correspondre et ignorer tout ce qui se trouve entre les deux.

Cependant, l'utilisation de .. peut être ambigu. S'il n'est pas possible de déterminer clairement quelles valeurs doivent correspondre et quelles valeurs doivent être ignorées, Rust va nous retourner une erreur. L'encart 18-25 nous montre un exemple d'utilisation ambigu de .. qui, par conséquent, ne se compilera pas.

Fichier : src/main.rs

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

    match nombres {
        (.., second, ..) => {
            println!("Voici quelques nombres : {}", second)
        },
    }
}

Encart 18-25 : une tentative d'utilisation de .. de manière ambigüe

Lorsque nous compilons cet exemple, nous obtenons l'erreur suivante :

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
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

error: could not compile `patterns` due to previous error

Il est impossible pour Rust de déterminer combien de valeurs doivent être ignorées dans le tuple avant de faire correspondre une valeur avec second et ensuite combien d'autres doivent être ignorées après. Ce code pourrait signifier que nous voulons ignorer 2, faire correspondre second avec 4, puis ignorer ensuite 8, 16 et 32 ; ou que nous souhaitons ignorer 2 et 4, faire correspondre second à 8, puis ignorer ensuite 16 et 32 ; et ainsi de suite. Le nom de la variable second ne signifie pas grand-chose pour Rust, donc nous obtenons une erreur de compilation à cause de l'utilisation de .. à deux endroits qui rendent la situation ambigüe.

Plus de conditions avec les contrôles de correspondance

Un contrôle de correspondance est une condition if supplémentaire renseignée après le motif d'une branche d'un match qui doit elle aussi correspondre en même temps que le filtrage par motif, pour que cette branche soit choisie. Les contrôles de correspondance sont utiles pour exprimer des idées plus complexes que celles permises uniquement par les motifs.

La condition peut utiliser des variables créées dans le motif. L'encart 18-26 montre un match dans lequel la première branche a le motif Some(x) et procède aussi au contrôle de correspondance if x < % 2 == 0 (qui sera vrai si le nombre est pair).

fn main() {
    let nombre = Some(4);

    match nombre {
        Some(x) if x % 2 == 0 => println!("Le nombre {} est pair", x),
        Some(x) => println!("Le nombre {} est impair", x),
        None => (),
    }
}

Encart 18-26 : ajout d'un contrôle de correspondance à un motif

Cet exemple va afficher Le nombre 4 est pair. Lorsque nombre est comparé au motif de la première branche, il va correspondre, car Some(4) correspond à Some(x). Ensuite, le contrôle de correspondance vérifie si le reste de la division de x par 2 vaut 0, et comme c'est le cas, la première branche est choisie.

Si nombre avait été plutôt Some(5), le contrôle de correspondance de la première branche aurait été faux car le reste de la division de 5 par 2 est 1, ce qui n'est pas égal à 0. Rust serait donc allé à la deuxième branche, qui devrait être choisie car cette deuxième branche correspond à n'importe quelle variante Some et n'a pas de contrôle de correspondance.

Comme il n'existe pas d'autre moyen d'exprimer la condition if x % 2 == 0 dans un motif, le contrôle de correspondance nous donne la possibilité d'exprimer une telle logique. L'inconvénient de cette expressivité renforcée est que le compilateur n'essaie pas de vérifier l'exhaustivité lorsqu'on utilise les contrôles de correspondance.

Dans l'encart 18-11, nous avions mentionné le fait que nous pouvions utiliser des contrôles de correspondance pour résoudre notre problème de masquage dans le motif. Souvenez-vous qu'une nouvelle variable avait été créée à l'intérieur du motif dans l'expression match au lieu d'utiliser la variable située à l'extérieur du match. Cette nouvelle variable implique que nous ne pouvons pas comparer avec la variable qui se situe à l'extérieur. L'encart 18-27 nous montre comment nous pouvons utiliser un contrôle de correspondance pour répondre à ce besoin.

Fichier : src/main.rs

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

    match x {
        Some(50) => println!("Nous obtenons 50"),
        Some(n) if n == y => println!("Nous avons une correspondance, n = {}", n),
        _ => println!("Cas par défaut, x = {:?}", x),
    }

    println!("Au final : x = {:?}, y = {}", x, y);
}

Encart 18-27 : utilisation d'un contrôle de correspondance pour vérifier l'égalité avec une variable externe au bloc

Ce code va maintenant afficher Cas par défaut, x = Some(5). Le motif de la deuxième branche du match ne crée pas de nouvelle variable y qui masquerait le y externe, ce qui signifie que nous pouvons utiliser le y externe dans le contrôle de correspondance. Au lieu de renseigner le motif comme étant Some(y), ce qui aurait masqué le y externe, nous renseignons Some(n). Cela va créer une nouvelle variable n qui ne masque rien car il n'y a pas de variable n à l'extérieur du match.

Le contrôle de correspondance if n == y n'est pas un motif et donc il n'introduit pas de nouvelle variable. Ce y est la variable externe y au lieu d'être une nouvelle variable masquée y, et nous pouvons comparer une valeur qui a la même valeur que le y externe en comparant n à y.

Vous pouvez aussi utiliser l'opérateur ou | dans un contrôle de correspondance pour y renseigner plusieurs motifs ; la condition du contrôle de correspondance s'effectuera alors sur tous les motifs. L'encart 18-28 montre la priorité de combinaison d'un contrôle de correspondance sur un motif qui utilise |. La partie importante de cet exemple est que le contrôle de correspondance if y s'applique sur 4, 5 et 6, même si if y semble s'appliquer uniquement à 6.

fn main() {
    let x = 4;
    let y = false;

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

Encart 18-28 : combinaison de plusieurs motifs avec un contrôle de correspondance

La condition de correspondance signifie que la branche correspond uniquement si la valeur de x vaut 4, 5 ou 6 et que y vaut true. Lorsque ce code s'exécute, le motif de la première branche correspond car x vaut 4, mais le contrôle de correspondance if y est faux, donc ce programme affiche no. La raison est que la condition if s'applique à tout le motif 4 | 5 | 6 et pas seulement à la dernière valeur 6. Autrement dit, la priorité d'un contrôle de correspondance avec un motif se comporte comme ceci :

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

et pas comme ceci :

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

Après avoir exécuté le code, le fonctionnement des priorités devient évident : si le contrôle de correspondance était seulement appliqué à la dernière valeur renseignée avec l'opérateur |, la branche correspondrait et le programme aurait affiché yes.

Capturer des valeurs avec @

L'opérateur @ nous permet de créer une variable qui stocke une valeur en même temps que nous testons cette valeur pour vérifier si elle correspond à un motif. L'encart 18-29 montre un exemple dans lequel nous souhaitons tester qu'un champ id d'un Message::Hello est dans un intervalle 3..=7. Mais nous voulons aussi associer la valeur à la variable id_variable pour que nous puissions l'utiliser dans le code associé à la branche. Nous aurions pu nommer cette variable avec le même nom que le champ id, mais pour cet exemple nous allons utiliser un nom différent.

fn main() {
    enum Message {
        Hello { id: i32 },
    }

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

    match msg {
        Message::Hello {
            id: id_variable @ 3..=7,
        } => println!("Nous avons trouvé un id dans l'intervalle : {}", id_variable),
        Message::Hello { id: 10..=12 } => {
            println!("Nous avons trouvé un id dans un autre intervalle")
        }
        Message::Hello { id } => println!("Nous avons trouvé un autre id : {}", id),
    }
}

Encart 18-29 : utilisation de @ pour lier une valeur d'un motif à une variable pendant qu'on la teste

Cet exemple va afficher Nous avons trouvé un id dans l'intervalle : 5. En renseignant id_variable @ avant l'intervalle 3..=7, nous capturons la valeur qui correspond à l'intervalle pendant que nous vérifions que la valeur correspond au motif de l'intervalle.

Dans la deuxième branche, où nous avons uniquement un intervalle renseigné dans le motif, le code associé à la branche n'a pas besoin d'une variable qui contienne la valeur actuelle du champ id. La valeur du champ id aurait pu être 10, 11 ou 12, mais le code associé à ce motif ne la connaîtra pas. Le code du motif n'est pas capable d'utiliser la valeur du champ id, car nous n'avons pas enregistré id dans une variable.

Dans la dernière branche, nous avons renseigné une variable sans intervalle, nous avons donc dans la variable id la valeur qui peut être utilisée dans le code de la branche. La raison à cela est que nous avons utilisé la syntaxe raccourcie pour les champs des structures. Mais, dans cette branche, nous n'avons pas appliqué de tests à la valeur sur le champ id, comme nous l'avions fait avec les deux premières branches : n'importe quelle valeur correspondra à ce motif.

L'utilisation de @ nous permet de tester une valeur et de l'enregistrer dans une variable au sein d'un seul et même motif.

Résumé

Les motifs de Rust sont très utiles lorsque nous devons distinguer différents types de données. Lorsque nous les avions utilisés dans les expressions match, Rust s'est assuré que vos motifs couvraient l'intégralité de toutes valeurs possibles, et, dans le cas contraire, votre programme ne se compilait pas. Les motifs dans les instructions let et les paramètres de fonction rendent ces constructions encore plus utiles, permettant de déstructurer les valeurs en parties plus petites tout en les assignant à des variables. Nous pouvons créer des motifs très simples ou alors plus complexes pour répondre à nos besoins.

Dans le chapitre suivant, qui sera l'avant-dernier du livre, nous allons découvrir quelques aspects avancés de l'éventail de fonctionnalités de Rust.