La syntaxe des motifs
Dans cette section, nous rassemblons toute la syntaxe valide dans les motifs et discutons pourquoi et quand vous pourriez vouloir utiliser chacune d’elles.
Correspondance avec des littéraux
Comme vous l’avez vu au chapitre 6, vous pouvez faire correspondre des motifs directement avec des littéraux. Le code suivant donne quelques exemples : rust {{#rustdoc_include ../listings/ch19-patterns-and-matching/no-listing-01-literals/src/main.rs:here}}
fn main() {
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 effectue une action s’il 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 fois dans ce livre. Cependant, il y à une complication lorsque vous utilisez des variables nommées dans des expressions match, if let ou while let. Comme chacun de ces types d’expressions ouvre une nouvelle portée, les variables déclarées en tant que partie d’un motif à l’intérieur de ces expressions masqueront celles ayant le même nom en dehors de la construction, comme c’est le cas pour toutes les variables. Dans l’encart 19-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. Regardez les motifs dans les branches du match et le println! à la fin, et essayez de deviner ce que le code affichera avant de l’exécuter ou de lire plus loin.
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(y) => println!("Matched, y = {y}"),
_ => println!("Default case, x = {x:?}"),
}
println!("at the end: x = {x:?}, y = {y}");
}
match expression with an arm that introduces a new variable which shadows an existing variable yVoyons ce qui se passe lorsque l’expression match s’exécute. Le motif de la première branche ne correspond pas à la valeur définie de x, donc le code continue.
Le motif de la deuxième branche introduit une nouvelle variable nommée y qui correspondra à n’importe quelle valeur à l’intérieur d’une valeur Some. Comme nous sommes dans une nouvelle portée à l’intérieur de l’expression match, c’est une nouvelle variable y, pas le y que nous avons déclaré au début avec la valeur 10. 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, ce nouveau y se lie à la valeur interne du Some dans x. Cette valeur est 5, donc l’expression de cette branche s’exécute et affiche Matched, y = 5.
Si x avait été une valeur None au lieu de Some(5), les motifs des deux premières branches n’auraient pas correspondu, donc la valeur aurait correspondu au tiret bas. Nous n’avons pas introduit la variable x dans le motif de la branche avec le tiret bas, donc le x dans l’expression est toujours le x extérieur qui n’a pas été masqué. Dans ce cas hypothétique, le match aurait affiché Default case, x = None.
Lorsque l’expression match est terminée, sa portée se terminé, et celle du y interne aussi. Le dernier println! produit at the end: x = Some(5), y = 10.
Pour créer une expression match qui compare les valeurs du x et du y extérieurs, plutôt que d’introduire une nouvelle variable qui masque la variable y existante, nous devrions utiliser une condition de garde de correspondance à la place. Nous parlerons des gardes de correspondance plus loin dans la section “Ajouter des conditions avec les gardes de correspondance”.
Correspondance avec plusieurs motifs
Dans les expressions match, vous pouvez faire correspondre plusieurs motifs en utilisant la syntaxe |, qui est l’opérateur ou pour les motifs. Par exemple, dans le code suivant, nous faisons correspondre la valeur de x aux branches du match, la première ayant une option ou, ce qui signifie que si la valeur de x correspond à l’une où l’autre des valeurs de cette branche, le code de cette branche s’exécutera : rust {{#rustdoc_include ../listings/ch19-patterns-and-matching/no-listing-02-multiple-patterns/src/main.rs:here}}
fn main() {
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
}
Ce code affiche one or two.
Correspondance avec des intervalles de valeurs avec ..=
La syntaxe ..= nous permet de faire correspondre un intervalle inclusif de valeurs. Dans le code suivant, lorsqu’un motif correspond à l’une des valeurs de l’intervalle donné, cette branche s’exécutera : rust {{#rustdoc_include ../listings/ch19-patterns-and-matching/no-listing-03-ranges/src/main.rs:here}}
fn main() {
let x = 5;
match x {
1..=5 => println!("one through five"),
_ => println!("something else"),
}
}
Si x vaut 1, 2, 3, 4 ou 5, la première branche correspondra. Cette syntaxe est plus pratique pour des correspondances avec plusieurs valeurs 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 un intervalle est beaucoup plus court, surtout si nous voulons correspondre, disons, à n’importe quel nombre entre 1 et 1 000 !
Le compilateur vérifie que l’intervalle n’est pas vide au moment de la compilation, et comme les seuls types pour lesquels Rust peut déterminer si un intervalle est vide ou non sont char et les valeurs numériques, les intervalles ne sont autorisés qu’avec des valeurs numériques ou char.
Voici un exemple utilisant des intervalles de valeurs char : rust {{#rustdoc_include ../listings/ch19-patterns-and-matching/no-listing-04-ranges-of-char/src/main.rs:here}}
fn main() {
let x = 'c';
match x {
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"),
}
}
Rust peut déterminer que 'c' se trouve dans l’intervalle du premier motif et affiche early ASCII letter.
La déstructuration pour décomposer des valeurs
Nous pouvons également utiliser des motifs pour déstructurer des structures, des enums et des tuples afin d’utiliser différentes parties de ces valeurs. Parcourons chaque type de valeur.
Les structures
L’encart 19-12 montre une structure Point avec deux champs, x et y, que nous pouvons décomposer en utilisant un motif avec une instruction let.
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);
}
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 dans le motif n’ont pas besoin de correspondre aux noms des champs de la structure. Cependant, il est courant de faire correspondre les noms de variables aux noms de champs pour faciliter la mémorisation de quelles variables proviennent de quels champs. En raison de cet usage courant, et parce qu’écrire let Point { x: x, y: y } = p; contient beaucoup de duplication, Rust propose un raccourci pour les motifs qui correspondent aux champs de structures : vous n’avez qu’à lister le nom du champ de la structure, et les variables créées à partir du motif auront les mêmes noms. L’encart 19-13 se comporte de la même manière que le code de l’encart 19-12, mais les variables créées dans le motif let sont x et y au lieu de a et b.
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);
}
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 structure p.
Nous pouvons aussi déstructurer avec des valeurs littérales dans le motif de la structure plutôt que de créer des variables pour tous les champs. Cela nous permet de tester certains champs pour des valeurs particulières tout en créant des variables pour déstructurer les autres champs.
Dans l’encart 19-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.
struct Point {
x: i32,
y: i32,
}
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})");
}
}
}
La première branche correspondra à tout point situé 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 cette branche.
De même, la deuxième branche 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. La troisième branche ne spécifie aucun littéral, elle correspond donc à tout autre Point et crée des variables pour les champs x et y.
Dans cet exemple, la valeur p correspond à la deuxième branche car x contient 0, donc ce code affichera On the y axis at 7.
Rappelez-vous qu’une expression match arrête de vérifier les branches dès 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’afficherait que On the x axis at 0.
Les enums
Nous avons déstructuré des enums dans ce livre (par exemple, l’encart 6-5 au chapitre 6), mais nous n’avons pas encore discuté explicitement du fait que le motif pour déstructurer un enum correspond à la manière dont les données stockées dans l’enum sont définies. À titre d’exemple, dans l’encart 19-15, nous utilisons l’enum Message de l’encart 6-2 et écrivons un match avec des motifs qui déstructureront chaque valeur interne.
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
let msg = Message::ChangeColor(0, 160, 255);
match msg {
Message::Quit => {
println!("The Quit variant has no data to destructure.");
}
Message::Move { x, y } => {
println!("Move in the x direction {x} and in the y direction {y}");
}
Message::Write(text) => {
println!("Text message: {text}");
}
Message::ChangeColor(r, g, b) => {
println!("Change color to red {r}, green {g}, and blue {b}");
}
}
}
Ce code affichera Change color to red 0, green 160, and blue 255. Essayez de changer la valeur de msg pour voir le code des autres branches s’exécuter.
Pour les variantes d’enum sans données, comme Message::Quit, nous ne pouvons pas déstructurer davantage la valeur. Nous ne pouvons que correspondre à la valeur littérale Message::Quit, et il n’y a pas de variables dans ce motif.
Pour les variantes d’enum de type structure, comme Message::Move, nous pouvons utiliser un motif similaire à celui que nous spécifions pour correspondre aux structures. Après le nom de la variante, nous plaçons des accolades, puis listons les champs avec des variables afin de décomposer les éléments à utiliser dans le code de cette branche. Ici, nous utilisons la forme raccourcie comme dans l’encart 19-13.
Pour les variantes d’enum de type tuple, comme Message::Write qui contient un tuple avec un élément et Message::ChangeColor qui contient un tuple avec trois éléments, le motif est similaire à celui que nous spécifions pour correspondre aux tuples. Le nombre de variables dans le motif doit correspondre au nombre d’éléments dans la variante que nous faisons correspondre.
Les structures et enums imbriqués
Jusqu’à présent, nos exemples ont tous fait correspondre des structures ou des enums à un seul niveau de profondeur, mais la correspondance peut aussi fonctionner sur des éléments imbriqués ! Par exemple, nous pouvons remanier le code de l’encart 19-15 pour prendre en charge les couleurs RGB et HSV dans le message ChangeColor, comme montré dans l’encart 19-16.
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}");
}
_ => (),
}
}
Le motif de la première branche dans l’expression match correspond à une variante d’enum Message::ChangeColor qui contient une variante Color::Rgb ; ensuite, le motif se lie aux trois valeurs i32 internes. Le motif de la deuxième branche correspond aussi à 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.
Les structures et les tuples
Nous pouvons combiner, faire correspondre et imbriquer des motifs de déstructuration de manières encore plus complexes. L’exemple suivant montre une déstructuration compliquée où nous imbriquons des structures et des tuples dans un tuple et déstructurons toutes les valeurs primitives : rust {{#rustdoc_include ../listings/ch19-patterns-and-matching/no-listing-05-destructuring-structs-and-tuples/src/main.rs:here}}
fn main() {
struct Point {
x: i32,
y: i32,
}
let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
}
Ce code nous permet de décomposer des types complexes en leurs éléments constitutifs afin que nous puissions utiliser séparément les valeurs qui nous intéressent.
La déstructuration avec des motifs est un moyen pratique d’utiliser des morceaux de valeurs, comme la valeur de chaque champ d’une structure, séparément les uns des autres.
Ignorer des valeurs dans un motif
Vous avez vu qu’il est parfois utile d’ignorer des valeurs dans un motif, comme dans la dernière branche d’un match, pour obtenir un attrape-tout qui ne fait rien en pratique mais couvre toutes les valeurs possibles restantes. Il existe plusieurs manières 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 commençant par un tiret bas, ou en utilisant .. pour ignorer les parties restantes d’une valeur. Explorons comment et pourquoi utiliser chacun de ces motifs.
Une valeur entière avec _
Nous avons utilisé le tiret bas comme motif joker qui correspond à n’importe quelle valeur sans se lier à la valeur. C’est particulièrement utile comme dernière branche d’une expression match, mais nous pouvons aussi l’utiliser dans n’importe quel motif, y compris les paramètres de fonction, comme montré dans l’encart 19-17.
fn foo(_: i32, y: i32) {
println!("This code only uses the y parameter: {y}");
}
fn main() {
foo(3, 4);
}
_ in a function signatureCe code ignorera complètement la valeur 3 passée comme premier argument, et affichera 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 changeriez la signature pour qu’elle n’inclue pas le paramètre inutilisé. Ignorer un paramètre de fonction peut être particulièrement utile dans les cas où, par exemple, vous implémentez un trait nécessitant une certaine signature de type mais le corps de la fonction dans votre implémentation n’a pas besoin de l’un des paramètres. Vous évitez alors un avertissement du compilateur concernant les paramètres de fonction inutilisés, comme cela se produirait si vous utilisiez un nom à la place.
Des parties d’une valeur avec un _ imbriqué
Nous pouvons aussi 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. L’encart 19-18 montre du code chargé de gérer la valeur d’un paramètre. Les exigences métier 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.
fn main() {
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:?}");
}
Some variants when we don’t need to use the value inside the SomeCe code affichera Can't overwrite an existing customized value puis setting is Some(5). Dans la première branche du match, nous n’avons pas besoin de faire correspondre ou d’utiliser les valeurs à l’intérieur des variantes Some, mais nous devons tester le cas où setting_value et new_setting_value sont des variantes Some. Dans ce cas, nous affichons la raison pour laquelle nous ne changeons pas setting_value, et elle n’est pas modifiée.
Dans tous les autres cas (si soit setting_value soit new_setting_value est None) exprimés par le motif _ dans la deuxième branche, nous voulons permettre à new_setting_value de devenir setting_value.
Nous pouvons aussi utiliser des tirets bas à plusieurs endroits dans un seul motif pour ignorer des valeurs particulières. L’encart 19-19 montre un exemple d’ignorance de la deuxième et de la quatrième valeurs dans un tuple de cinq éléments.
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {first}, {third}, {fifth}");
}
}
}
Ce code affichera 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 émettra généralement un avertissement car une variable inutilisée pourrait être un bogue. Cependant, il est parfois utile de pouvoir créer une variable que vous n’utiliserez pas encore, par exemple lorsque vous faites du prototypage ou que vous démarrez un projet. Dans cette situation, vous pouvez dire à Rust de ne pas vous avertir de la variable inutilisée en commençant le nom de la variable par un tiret bas. Dans l’encart 19-20, nous créons deux variables inutilisées, mais lorsque nous compilons ce code, nous ne devrions recevoir un avertissement que pour l’une d’entre elles.
fn main() {
let _x = 5;
let y = 10;
}
Ici, nous recevons un avertissement pour la non-utilisation de la variable y, mais nous n’en recevons pas pour la non-utilisation de _x.
Notez qu’il y à une différence subtile entre utiliser uniquement _ et utiliser un nom qui commence par un tiret bas. 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, l’encart 19-21 nous donnera une erreur.
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{s:?}");
}
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, utiliser le tiret bas seul ne lie jamais la valeur. L’encart 19-22 compilera sans erreur car s n’est pas déplacé dans _.
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_) = s {
println!("found a string");
}
println!("{s:?}");
}
Ce code fonctionne parfaitement car nous ne lions jamais s à quoi que ce soit ; il n’est pas déplacé.
Les 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 de devoir lister des tirets bas pour chaque valeur ignorée. Le motif .. ignore toutes les parties d’une valeur que nous n’avons pas explicitement fait correspondre dans le reste du motif. Dans l’encart 19-23, nous avons une structure Point qui contient 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 des champs y et z.
fn main() {
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}"),
}
}
Point except for x by using ..Nous listons la valeur x puis incluons simplement le motif ... C’est plus rapide que de devoir lister y: _ et z: _, en particulier lorsque nous travaillons avec des structures ayant de nombreux champs dans des situations où seuls un où deux champs sont pertinents.
La syntaxe .. s’étendra à autant de valeurs que nécessaire. L’encart 19-24 montre comment utiliser .. avec un tuple.
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, .., last) => {
println!("Some numbers: {first}, {last}");
}
}
}
Dans ce code, la première et la dernière valeur sont mises en correspondance avec first et last. Le .. correspondra et ignorera tout ce qui se trouve au milieu.
Cependant, l’utilisation de .. doit être non ambiguë. S’il n’est pas clair quelles valeurs sont destinées à la correspondance et lesquelles doivent être ignorées, Rust nous donnera une erreur. L’encart 19-25 montre un exemple d’utilisation ambiguë de .., qui ne compilera donc pas.
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(.., second, ..) => {
println!("Some numbers: {second}")
},
}
}
.. in an ambiguous wayLorsque nous compilons cet exemple, nous obtenons cette erreur : console {{#include ../listings/ch19-patterns-and-matching/listing-19-25/output.txt}}
$ 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` (bin "patterns") due to 1 previous error
Il est impossible pour Rust de déterminer combien de valeurs du tuple ignorer avant de faire correspondre une valeur avec second et combien de valeurs supplémentaires ignorer ensuite. Ce code pourrait signifier que nous voulons ignorer 2, lier second à 4, puis ignorer 8, 16 et 32 ; ou que nous voulons ignorer 2 et 4, lier second à 8, puis ignorer 16 et 32 ; et ainsi de suite. Le nom de variable second n’à aucune signification particulière pour Rust, donc nous obtenons une erreur du compilateur car utiliser .. à deux endroits comme cela est ambigu.
Ajouter des conditions avec les gardes de correspondance
Une garde de correspondance (match guard) est une condition if supplémentaire, spécifiée après le motif dans une branche de match, qui doit également être satisfaite pour que cette branche soit choisie. Les gardes de correspondance sont utiles pour exprimer des idées plus complexes que ce qu’un motif seul permet. Notez, cependant, qu’elles ne sont disponibles que dans les expressions match, pas dans les expressions if let ou while let.
La condition peut utiliser des variables créées dans le motif. L’encart 19-26 montre un match où la première branche à le motif Some(x) et aussi une garde de correspondance if x % 2 == 0 (qui sera true si le nombre est pair).
fn main() {
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 => (),
}
}
Cet exemple affichera The number 4 is even. Lorsque num est comparé au motif de la première branche, 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 comme c’est le cas, la première branche est sélectionnée.
Si num avait été Some(5), la garde de correspondance de la première branche aurait été false car le reste de 5 divisé par 2 est 1, ce qui n’est pas égal à 0. Rust passerait alors à la deuxième branche, qui correspondrait car la deuxième branche n’a pas de garde de correspondance et correspond donc à n’importe quelle variante Some.
Il n’y à aucun moyen d’exprimer la condition if x % 2 == 0 dans un motif, donc la garde de correspondance nous donne la possibilité d’exprimer cette logique. L’inconvénient de cette expressivité supplémentaire est que le compilateur n’essaie pas de vérifier l’exhaustivité lorsque des expressions de garde de correspondance sont impliquées.
Lorsque nous avons discuté de l’encart 19-11, nous avons mentionné que nous pouvions utiliser des gardes de correspondance pour résoudre notre problème de masquage de motif. Rappelez-vous que nous avions 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 contre la valeur de la variable extérieure. L’encart 19-27 montre comment nous pouvons utiliser une garde de correspondance pour corriger ce problème.
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 = {x:?}, y = {y}");
}
Ce code affichera maintenant Default case, x = Some(5). Le motif de la deuxième branche n’introduit pas de nouvelle variable y qui masquerait le y extérieur, ce qui signifie que nous pouvons utiliser le y extérieur dans la garde de correspondance. Au lieu de spécifier le motif comme Some(y), ce qui aurait masqué le y extérieur, nous spécifions Some(n). Cela crée une nouvelle variable n qui ne masque 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 n’introduit donc pas de nouvelles variables. Ce y est le y extérieur plutôt qu’un nouveau y le masquant, et nous pouvons chercher une valeur ayant la même valeur que le y extérieur en comparant n à y.
Vous pouvez aussi 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. L’encart 19-28 montre la précédence lors de la combinaison d’un motif utilisant | 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 s’il pourrait sembler que if y ne s’applique qu’à 6.
fn main() {
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}
}
La condition de correspondance stipule que la branche 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 de la première branche correspond car x vaut 4, mais la garde de correspondance if y est false, donc la première branche n’est pas choisie. Le code passe à la deuxième branche, qui correspond, et ce programme affiche no. La raison est que la condition if s’applique à l’ensemble du motif 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 l’exécution du code, le comportement de précédence est évident : si la garde de correspondance n’était appliquée qu’à la dernière valeur de la liste de valeurs spécifiées avec l’opérateur |, la branche aurait correspondu, et le programme aurait affiché yes.
Utiliser les liaisons @
L’opérateur at @ nous permet de créer une variable qui contient une valeur en même temps que nous testons cette valeur pour une correspondance de motif. Dans l’encart 19-29, nous voulons tester que le champ id d’un Message::Hello se trouve dans l’intervalle 3..=7. Nous voulons aussi lier la valeur à la variable id afin de pouvoir l’utiliser dans le code associé à la branche.
fn main() {
enum Message {
Hello { id: i32 },
}
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello { id: id @ 3..=7 } => {
println!("Found an id in range: {id}")
}
Message::Hello { id: 10..=12 } => {
println!("Found an id in another range")
}
Message::Hello { id } => println!("Found some other id: {id}"),
}
}
@ to bind to a value in a pattern while also testing itCet exemple affichera Found an id in range: 5. En spécifiant id @ avant l’intervalle 3..=7, nous capturons la valeur qui correspond à l’intervalle dans une variable nommée id tout en testant que la valeur correspond au motif d’intervalle.
Dans la deuxième branche, où nous n’avons qu’un intervalle spécifié dans le motif, le code associé à la branche n’a pas de variable contenant la valeur réelle du champ id. La valeur du champ id aurait pu être 10, 11 ou 12, mais le code qui accompagne ce motif ne sait pas laquelle. Le code du motif ne peut pas utiliser la valeur du champ id car nous n’avons pas sauvegardé la valeur de id dans une variable.
Dans la dernière branche, où nous avons spécifié une variable sans intervalle, nous avons bien la valeur disponible pour utilisation dans le code de la branche dans une variable nommée id. La raison est que nous avons utilisé la syntaxe raccourcie des champs de structure. Mais nous n’avons appliqué aucun test à la valeur du champ id dans cette branche, comme nous l’avons fait avec les deux premières branches : n’importe quelle valeur correspondrait à ce motif.
Utiliser @ nous permet de tester une valeur et de la sauvegarder dans une variable au sein d’un seul motif.
Résumé
Les motifs de Rust sont très utiles pour distinguer les différents types de données. Lorsqu’ils sont utilisés dans des expressions match, Rust s’assuré que vos motifs couvrent toutes les valeurs possibles, sinon votre programme ne compilera pas. Les motifs dans les instructions let et les paramètres de fonction rendent ces constructions plus utiles, permettant la déstructuration des valeurs en parties plus petites et l’assignation de ces parties à des variables. Nous pouvons créer des motifs simples ou complexes pour répondre à nos besoins.
Ensuite, pour l’avant-dernier chapitre du livre, nous examinerons quelques aspects avancés de diverses fonctionnalités de Rust.