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); }
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); }
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); }
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), } }
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 ), } }
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 ), _ => (), } }
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); }
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); }
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) } } }
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; }
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);
}
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); }
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), } }
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); } } }
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)
},
}
}
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 => (), } }
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); }
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"), } }
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), } }
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.