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.
Correspondance avec des 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.
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 subsiste un problème lorsque vous utilisez les variables nommées dans les expressions match, if let, ou while let. Comme chacun de ces genres d’expressions débute une nouvelle portée, les variables déclarées comme faisant partie du motif de ces constructions vont masquer celles ayant le même nom et provenant de l’extérieur des constructions, comme c’est le cas avec toutes les variables. Dans l’encart 19-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.
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!("À la fin : 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 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 À 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 nouvelle variable qui masque la variable existante y, nous aurions besoin d’utiliser à la place un contrôle de correspondance. Nous verrons les contrôles de correspondance dans la section “Plus de conditions avec les contrôles de correspondance”.
Correspondance de plusieurs motifs
Dans les expressions match, vous pouvez faire correspondre une même branche à plusieurs motifs en utilisant la syntaxe |, qui est l’opérateur ou des motifs. 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!("n'importe quoi"),
}
}
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 ; si nous avions utilisé |, nous aurions dû écrire 1 | 2 | 3 | 4 | 5. 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 !
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 dire si un intervalle est vide ou non sont les caractères char et les valeurs numériques, les intervalles ne sont autorisés qu’avec des valeurs numériques ou des 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!("quelque chose d'autre"),
}
}
Rust peut déterminer que 'c' est dans le premier intervalle du premier motif et affiche lettre ASCII du début.
Déstructurer pour séparer les valeurs
Nous pouvons aussi utiliser les motifs pour déstructurer les structures, les énumérations, et les tuples pour utiliser différentes parties de ces valeurs. Passons en revue chacun des cas.
Structures
L’encart 19-12 montre une structure Point avec deux champs, x et y, que nous pouvons séparer 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 du motif n’ont pas à correspondre aux noms des champs de la structure. Mais il est courant de faire correspondre le nom des variables avec le nom des champs pour se rappeler plus facilement quelle variable provient de quel champ. Comme c’est là un usage courant, et qu’écrire let Point { x: x, y: y } = p; est inutilement redondant, Rust a un raccourci pour les motifs qui correspondent aux champs des structures : il vous suffit de lister simplement les noms des champs de la structure pour que les variables créées à partir du motif aient les mêmes noms. L’encart 19-13 montre du code qui se comporte de la même manière que le code de l’encart 19-12, mais dans lequel les variables créées dans le motif du 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. Il en résulte que les variables x et y contiennent les valeurs correspondantes de la structure p.
Nous pouvons aussi déstructurer 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 déstructurer les autres champs.
Dans l’encart 19-14, nous avons 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.
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})");
}
}
}
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.
Gardez en tête qu’une expression match arrête de tester ses branches une fois qu’elle a trouvé le premier motif qui correspond, donc quand bien même Point { x: 0, y: 0 } est sur l’axe des x et sur l’axe des y axis, ce code n’affichera que this code would only print Sur l'axe x à la position 0.
Énumérations
Nous avons déjà déstructuré des énumérations dans ce livre (par exemple dans l’encart 6-5 du chapitre 6), mais nous n’avions pas précisé explicitement que le motif pour déstructurer une énumération correspond à la façon dont sont définies les données dans l’énumération. Par exemple, dans l’encart 19-15, nous utilisons l’énumération Message de l’encart 6-2 et nous ajoutons un match avec des motifs qui vont déstructurer chaque valeur interne.
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 {x} sur l'axe x et de {y} sur l'axe y");
}
Message::Ecrire(text) => {
println!("Message textuel : {text}");
}
Message::ChangerCouleur(r, v, b) => {
println!("Changement des taux de rouge à {r}, de vert à {v} et de bleu à {b}");
}
}
}
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 déstructurer de valeur. 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 19-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.
Structures et énumérations imbriquées
Jusqu’à présent, nos exemples avaient tous des correspondances avec des structures ou des énumérations avec un seul niveau de profondeur, mais les correspondances fonctionnent aussi sur les éléments imbriqués ! Par exemple, nous pouvons remanier le code de l’encart 19-15 pour pouvoir utiliser des couleurs RVB et TSV dans le message ChangerCouleur, comme dans l’encart 19-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 à {r}, de vert à {v} et de bleu à {b}");
}
Message::ChangerCouleur(Couleur::Tsv(t, s, v)) => {
println!("Changement des taux de teinte à {t}, de saturation à {s} et de valeur à {v}");
}
_ => (),
}
}
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 à Couleur::Tsv. Nous pouvons renseigner ces conditions complexes dans une seule expression match, bien que deux énumérations différentes soient impliquées.
Structures et 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 déstructurons 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 (caractère de soulignement _) comme un motif joker qui correspondra avec n’importe quelle valeur mais ne l’assignera pas. Ceci est particulièrement utile dans la dernière branche d’une expression match, mais nous pouvons aussi l’utiliser dans n’importe quel motif, y compris dans les paramètres de fonctions, comme montré dans l’encart 19-17.
fn fonction(_: i32, y: i32) {
println!("Ce code utilise uniquement le paramètre y : {y}");
}
fn main() {
fonction(3, 4);
}
_ in a function signatureCe 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’inclue pas le paramètre non utilisé. Ignorer un paramètre de fonction peut être particulièrement utile dans les cas où, comme par exemple, vous implémentez 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. Ainsi, vous éviterez d’avoir un avertissement du compilateur concernant les paramètres de fonction inutilisé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 19-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);
}
Some variants when we don’t need to use the value inside the SomeCe 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 affichons la raison pour laquelle nous n’allons pas changer valeur_du_reglage, et cette dernière 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 19-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 {
(first, _, third, _, fifth) => {
println!("Voici quelques nombres : {}, {}, {}", premier, troisieme, cinquieme)
}
}
}
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 une variable inutilisée pourrait être un bogue. Cependant, il est parfois utile d’avoir la possibilité 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 19-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.
fn main() {
let _x = 5;
let y = 10;
}
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 _x.
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 19-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:?}");
}
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. À l’inverse, l’utilisation du tiret bas tout seul n’assigne jamais la valeur à quelque chose. Par conséquent, l’encart 19-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:?}");
}
Ce code fonctionne correctement car nous n’assignons jamais s à quoi que ce soit ; 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 utiliser des éléments bien précis 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 19-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}"),
}
}
Point except for x by using ..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 19-24 montre comment utiliser .. avec un tuple.
fn main() {
let nombres = (2, 4, 8, 16, 32);
match nombres {
(premier, .., dernier) => {
println!("Voici quelques nombres : {premier}, {dernier}");
}
}
}
Dans ce code, la première et la dernière valeurs 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 19-25 nous montre un exemple d’utilisation ambigu de .. qui, par conséquent, ne se compilera pas.
fn main() {
let nombres = (2, 4, 8, 16, 32);
match nombres {
(.., second, ..) => {
println!("Voici quelques nombres : {second}")
},
}
}
.. in an ambiguous wayLorsque 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` (bin "patterns") due to 1 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 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. Notez toutefois 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 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 true 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 => (),
}
}
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é false 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 19-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 que nous avions créé une nouvelle variable à 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 19-27 nous montre comment nous pouvons utiliser un contrôle de correspondance pour répondre à ce besoin.
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("On a 50"),
Some(n) => println!("Correspondance, n = {n:?}"),
_ => println!("Cas par défaut, x = {x:?}"),
}
println!("À la fin : x = {x:?}, y = {y:?}");
}
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. Cet y est la variable externe y au lieu d’être une nouvelle variable y la masquant, et nous pouvons rechercher 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 19-28 montre l’ordre des priorités quand on combine un motif qui utilise | avec un contrôle de correspondance. 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!("oui"),
_ => println!("non"),
}
}
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.
Capture de 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 une correspondance de motif. Dans l’encart 19-29, nous souhaitons déterminer si un champ id d’un Message::Hello est dans l’intervalle 3..=7. Nous voulons aussi associer la valeur à la variable id pour que nous puissions l’utiliser dans le code associé à la branche.
fn main() {
enum Message {
Salutation { id: i32 },
}
let msg = Message::Salutation { id: 5 };
match msg {
Message::Salutation { id: id @ 3..=7 } => {
println!("Nous avons trouvé un id dans l'intervalle : {id}")
}
Message::Salutation { id: 10..=12 } => {
println!("Nous avons trouvé un id dans un autre intervalle")
}
Message::Salutation { id } => println!("Nous avons trouvé un autre id : {id}"),
}
}
@ to bind to a value in a pattern while also testing itCet exemple va afficher Nous avons trouvé un id dans l'intervalle : 5. En renseignant id @ avant l’intervalle 3..=7, nous capturons la valeur qui correspond à l’intervalle dans une variable nommée id pendant que nous vérifions aussi 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 pour faire la distinction entre différents types de données. Lorsqu’ils sont utilisés dans les expressions match, Rust s’assure que vos motifs couvrent l’intégralité de toutes les valeurs possibles, et, dans le cas contraire, votre programme ne se compilera pas. Les motifs dans les instructions let et les paramètres de fonction rendent ces constructions encore plus utiles, permettant la déstructuration de 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.