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 VALUE {
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
}
Par exemple, voici l’expression match de l’encart 6-5 qui correspond avec une valeur Option<i32> dans la variable x:
match x {
None => None,
Some(i) => Some(i + 1),
}
Les motifs dans cette expression match sont None et Some(i) à la gauche de chaque flèche.
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 la partie “Ignorer des valeurs dans un motif” plus tard dans ce chapitre.
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;
}
À chaque fois que vous avez utilisé une instruction let de cette manière, vous avez utilisé les motifs, bien que vous ne vous en soyez probablement pas rendu compte ! Plus formellement, une instruction let ressemble à ceci :
let PATTERN = 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 19-1, qui utilise un motif let pour déstructurer un tuple.
fn main() {
let (x, y, z) = (1, 2, 3);
}
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 — à savoir qu’il voit que le nombre d’éléments est le même dans les deux cas — 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 19-2 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);
}
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` (bin "patterns") due to 1 previous error
Pour réparer l’erreur, nous pourrions ignorer une ou plusieurs valeurs dans un tuple en utilisant _ ou .., comme vous allez le voir à la section “Ignorer des valeurs dans un motif”. 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 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 19-3 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, Rust n’exige pas que les conditions, dans une série de branches if let, else if et else if let, aient besoin d’être en rapport les unes avec les autres.
Le code de l’encart 19-3 détermine la couleur de votre arrière-plan en fonction d’une série de vérifications pour quelques conditions. 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.
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");
}
}
if let, else if, else if let, and elseSi l’utilisateur renseigne une couleur favorite, c’est cette couleur qui devient la couleur de fond. Si aucune couleur favorite n’est spécifiée et que 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 peut également définir de nouvelles variables qui viennent masquer des variables existantes de la même manière que le font les branches match : la ligne if let Ok(age) = age crée une nouvelle variable age qui contient la valeur présente dans la variante Ok, masquant la variable age préexistante. 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 nouvelle variable 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é.
L’inconvénient 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
D’une construction similaire à if let, les boucles conditionnelles while let permettent à une boucle while de s’exécuter aussi longtemps qu’un motif continue à correspondre. L’encart 19-4 montre une boucle while let qui attend des messages envoyés entre fils d’exécution, mais qui cette fois-ci contrôle un Result à la place d’une Option.
fn main() {
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
for valeur in [1, 2, 3] {
tx.send(valeur).unwrap();
}
});
while let Ok(value) = rx.recv() {
println!("{value}");
}
}
while let loop to print values for as long as rx.recv() returns OkCet exemple affiche 1, 2, et ensuite 3. La méthode recv prend le premier message du côté réception du canal et renvoie un Ok(valeur). Quand nous avons vu pour la première fois recv, dans le chapitre 16, nous avions extrait l’erreur directement, ou nous avions interagi avec elle comme un itérateur en utilisant une boucle for. Cependant, comme le montre l’encart Listing 19-4, nous pouvons aussi utiliser while let car la méthode recv renvoie un Ok à chaque fois qu’un message arrive, tant que l’émetteur existe, et, une fois que le côté emetteur se déconnecte, produit un Err.
Les boucles for
Dans une boucle for, la valeur qui suit directement le mot-clé for est un motif. Par exemple, dans for x in y, x est un motif. L’encart 19-5 montre comment utiliser un motif dans une boucle for pour déstructurer, ou encore fractionner un tuple dans le cadre de la boucle for.
fn main() {
let v = vec!['a', 'b', 'c'];
for (indice, valeur) in v.iter().enumerate() {
println!("{valeur} est à l'indice {indice}");
}
}
for loop to destructure a tupleLe code de l’encart 19-5 va afficher ceci :
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
Finished `dev` profile [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 paramètres de fonctions
Les paramètres de fonctions peuvent aussi être des motifs. Le code de l’encart 19-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() {}
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 19-7 déstructure les valeurs d’un tuple que nous passons en argument d’une fonction.
fn afficher_coordonnees(&(x, y): &(i32, i32)) {
println!("Coordonnées actuelles : ({x}, {y})");
}
fn main() {
let point = (3, 5);
afficher_coordonnees(&point);
}
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.
À 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.