Tous les endroits où les motifs peuvent être utilisés

Les motifs apparaissent dans de nombreux endroits en Rust, et vous en avez utilisé beaucoup sans vous en rendre compte ! Cette section va présenter les différentes situations où l'utilisation des motifs est appropriée.

Les branches des match

Comme nous l'avons vu au chapitre 6, nous utilisons les motifs dans les branches des expressions match. Techniquement, les expressions match sont définies avec le mot-clé match, une valeur sur laquelle procéder et une ou plusieurs branches qui constituent un motif, chacune associée à une expression à exécuter si la valeur correspond au motif de la branche, comme ceci :

match VALEUR {
    MOTIF => EXPRESSION,
    MOTIF => EXPRESSION,
    MOTIF => EXPRESSION,
}

L'une des conditions à respecter pour les expressions match est qu'elles doivent être exhaustives dans le sens où toutes les valeurs possibles de la valeur présente dans l'expression match doivent être prises en compte. Une façon de s'assurer que vous avez couvert toutes les possibilités est d'avoir un motif passe-partout pour la dernière branche : par exemple, une valeur quelconque ne pourra jamais échouer car la dernière branche permet de couvrir tous les autres cas possibles.

Le motif spécifique _ va correspondre à tout, mais il ne fournira jamais de variable, donc il est souvent utilisé dans la dernière branche. Le motif _ peut par exemple être utile lorsque vous souhaitez ignorer toutes les autres valeurs qui n'ont pas été listées. Nous allons voir plus en détail le motif _ dans une section plus tard dans ce chapitre.

Les expressions conditionnelles if let

Au chapitre 6, nous avons vu comment utiliser les expressions if let, principalement pour pouvoir écrire l'équivalent d'un match qui ne correspond qu'à un seul cas. Accessoirement, if let peut avoir un else correspondant au code à exécuter si le motif du if let ne correspond pas au premier critère.

L'encart 18-1 montre qu'il est aussi possible de conjuguer les expressions if let, else if et else if let. Faire ceci nous donne plus de flexibilité qu'une expression match dans laquelle nous ne pouvons fournir qu'une seule valeur à comparer avec les motifs. De plus, dans une série de branches if let, else if et else if let, les conditions n'ont pas besoin d'être en rapport les unes avec les autres.

Le code de l'encart 18-1 montre une série de vérifications pour quelques conditions qui décident quelle devrait être la couleur de fond. Pour cet exemple, nous avons créé les variables avec des valeurs codées en dur qu'un vrai programme devrait recevoir d'une saisie d'un utilisateur.

Fichier : src/main.rs

fn main() {
    let couleur_favorite: Option<&str> = None;
    let on_est_mardi = false;
    let age: Result<u8, _> = "34".parse();

    if let Some(couleur) = couleur_favorite {
        println!("Utilisation de votre couleur favorite, {}, comme couleur de fond", couleur);
    } else if on_est_mardi {
        println!("Mardi, c'est le jour du vert !");
    } else if let Ok(age) = age {
        if age > 30 {
            println!("Utilisation du violet comme couleur de fond");
        } else {
            println!("Utilisation de l'orange comme couleur de fond");
        }
    } else {
        println!("Utilisation du bleu comme couleur de fond");
    }
}

Encart 18-1 : mélange de if let, else if, else if let, et else

Si l'utilisateur renseigne une couleur favorite, c'est cette couleur qui devient la couleur de fond. Sinon, si nous sommes mardi, la couleur de fond sera le vert. Sinon, si l'utilisateur a renseigné son âge dans une chaîne de caractères et que nous pouvons l'interpréter comme un nombre avec succès, la couleur de fond sera soit le violet, soit l'orange en fonction de la valeur de ce nombre. Enfin, si aucune de ces conditions ne s'applique, la couleur de fond sera le bleu.

Cette structure conditionnelle nous permet de répondre à des conditions complexes. Avec les valeurs codées en dur que nous avons ici, cet exemple devrait afficher Utilisation du violet comme couleur de fond.

Vous pouvez constater que le if let nous permet d'utiliser les variables masquées de la même manière que le font les branches match : la ligne if let Ok(age) = age crée une nouvelle variable masquée age qui contient la valeur présente dans la variante Ok. Cela signifie que nous devons placer la condition if age > 30 à l'intérieur de ce bloc : nous ne pouvons pas combiner ces deux conditions dans une seule if let Ok(age) = age && age > 30. La variable masquée age que nous souhaitons comparer à 30 n'est pas encore en vigueur tant que la nouvelle portée entre les accolades n'a pas commencée.

Le désavantage de l'utilisation des expressions if let est que le compilateur ne vérifie pas l'exhaustivité contrairement à une expression match. Si nous avions enlevé le dernier bloc else, oubliant ainsi de gérer certains cas, le compilateur n'aurait pas pu nous prévenir d'un possible bogue de logique.

les boucles conditionelles while let

Comme les constructions if let, les boucles conditionnelles while let permettent à une boucle while de s'exécuter aussi longtemps qu'un motif continue à correspondre. L'exemple dans l'encart 18-2 montre une boucle while let qui utilise un vecteur comme une pile et affiche les valeurs du vecteur dans l'ordre opposé à celui dans lequel elles ont été insérées.

fn main() {
    let mut pile = Vec::new();

    pile.push(1);
    pile.push(2);
    pile.push(3);

    while let Some(donnee_du_haut) = pile.pop() {
        println!("{}", donnee_du_haut);
    }
}

Encart 18-2 : utilisation d'une boucle while let pour afficher les valeurs aussi longtemps que pile.pop() retourne une Some

Cet exemple affiche 3, 2 puis ensuite 1. La méthode pop sort le dernier élément du vecteur et retourne Some(valeur). Si le vecteur est vide, pop retourne alors None. La boucle while continue à exécuter le code de son bloc aussi longtemps que pop retourne un Some. Lorsque pop retournera None, la boucle s'arrêtera. Nous pouvons utiliser while let pour extraire tous les éléments de la pile.

Les boucles for

Au chapitre 3, nous avions mentionné que la boucle for était la construction de boucle la plus utilisée dans du code Rust, mais nous n'avons pas encore abordé le motif que prend for. Dans une boucle for, le motif est la valeur qui suit directement le mot-clé for, de sorte que x est le motif dans for x in y.

L'encart 18-3 montre comment utiliser un motif dans une boucle for pour déstructurer, ou décomposer, un tuple faisant partie de la boucle for.

fn main() {
    let v = vec!['a', 'b', 'c'];

    for (indice, valeur) in v.iter().enumerate() {
        println!("{} est à l'indice {}", valeur, indice);
    }
}

Encart 18-3 : utilisation d'un motif dans une boucle for pour déstructurer un tuple

Le code de l'encart 18-3 va afficher ceci :

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
    Finished dev [unoptimized + debuginfo] target(s) in 0.52s
     Running `target/debug/patterns`
a est à l'indice 0
b est à l'indice 1
c est à l'indice 2

Nous avons utilisé la méthode enumerate pour produire une valeur et son indice à partir d'un itérateur que nous avons placé dans un tuple. La premiere valeur produite est le tuple (0, 'a'). Comme cette valeur correspond au motif (indice, valeur), indice se voit affecter 0, valeur se voit affecter 'a', provoquant l'affichage de la première ligne sur la sortie.

Les instructions let

Avant d'arriver à ce chapitre, nous n'avions abordé explicitement l'utilisation des motifs qu'avec match et if let, mais en réalité, nous avions utilisé les motifs dans d'autres endroits, y compris dans les instructions let. Par exemple, considérons l'assignation de la variable suivante avec let :


#![allow(unused)]
fn main() {
let x = 5;
}

Tout au long de ce livre, nous avons utilisé let de cette manière des centaines de fois, et malgré tout vous ne vous êtes probablement pas rendu compte que vous utilisiez les motifs ! Plus formellement, une instruction let ressemble à ceci :

let MOTIF = EXPRESSION;

Dans des instructions telles que let x = 5; avec un nom de variable dans l'emplacement MOTIF, le nom de la variable n'est juste qu'une forme particulièrement simple de motif. Rust compare l'expression avec le motif et assigne tous les noms qu'il trouve. Dans l'exemple let x = 5;, x est un motif qui signifie “relie ce qui correspond ici à la variable x”. Puisque le nom x constitue un motif complet, il signifie exactement “relie tout ce qui suit à la variable x, quelle qu'en soit la valeur”.

Pour comprendre plus clairement l'aspect filtrage par motif de let, examinons l'encart 18-4, qui utilise un motif let pour destructurer un tuple.

fn main() {
    let (x, y, z) = (1, 2, 3);
}

Encart 18-4 : utilisation d'un motif pour destructurer un tuple et créer trois variables à la fois

Ici, nous avons fait correspondre un tuple à un motif. Rust compare la valeur (1, 2, 3) avec le motif (x, y, z) et constate que la valeur correspond au motif, donc Rust relie 1 à x, 2 à y et 3 à z. Vous pouvez ainsi considérer que ce motif de tuple encapsule trois variables individuelles.

Si le nombre d'éléments dans le motif ne correspond pas au nombre d'éléments dans le tuple, le type global ne va pas correspondre et nous allons obtenir une erreur de compilation. Par exemple, l'encart 18-5 montre une tentative de déstructurer un tuple avec trois éléments dans deux variables, ce qui ne va pas fonctionner.

fn main() {
    let (x, y) = (1, 2, 3);
}

Encart 18-5 : construction incorrecte d'un motif dont les variables ne vont pas correspondre au nombre d'éléments présents dans le tuple

Si vous essayez de compiler ce code, vous obtiendrez cette erreur de type :

$ cargo run
   Compiling patterns v0.1.0 (file:///projects/patterns)
error[E0308]: mismatched types
 --> src/main.rs:2:9
  |
2 |     let (x, y) = (1, 2, 3);
  |         ^^^^^^   --------- this expression has type `({integer}, {integer}, {integer})`
  |         |
  |         expected a tuple with 3 elements, found one with 2 elements
  |
  = note: expected tuple `({integer}, {integer}, {integer})`
             found tuple `(_, _)`

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

Si nous souhaitons ignorer une ou plusieurs valeurs dans un tuple, nous pouvons utiliser _ ou .., comme vous allez le voir à la dernière section de ce chapitre. Si le problème est que nous avons trop de variables dans le motif, la solution pour faire correspondre les types consiste à enlever des variables de façon à ce que le nombre de variables corresponde au nombre d'éléments présents dans le tuple.

Les paramètres de fonctions

Les paramètres de fonctions peuvent aussi être des motifs. Le code de l'encart 18-6 déclare une fonction foo qui prend un paramètre x de type i32.

fn fonction(x: i32) {
    // le code se place ici
}

fn main() {}

Encart 18-6 : une signature de fonction qui utilise des motifs dans ses paramètres

La partie x est un motif ! Comme nous l'avons dit pour let, nous pouvons faire correspondre le motif avec un tuple dans les arguments de la fonction. L'encart 18-7 déstructure les valeurs d'un tuple que nous passons en argument d'une fonction.

Fichier : src/main.rs

fn afficher_coordonnees(&(x, y): &(i32, i32)) {
    println!("Coordonnées actuelles : ({}, {})", x, y);
}

fn main() {
    let point = (3, 5);
    afficher_coordonnees(&point);
}

Encart 18-7 : une fonction avec des paramètres qui déstructurent un tuple

Ce code affiche Coordonées actuelles : (3, 5). Les valeurs &(3, 5) correspondent au motif &(x, y), donc x a la valeur 3 et y a la valeur 5.

Nous pouvons aussi utiliser les motifs dans la liste des paramètres d'une fermeture de la même manière que dans la liste des paramètres d'une fonction, car les fermetures sont similaires aux fonctions, comme nous l'avons dit au chapitre 13.

A présent, vous avez vu plusieurs façons d'utiliser les motifs, mais les motifs ne fonctionnent pas de la même manière dans toutes les situations où nous les utilisons. Des fois, le motif sera irréfutable ; d'autres fois, il sera réfutable. C'est ce que nous allons voir tout de suite.