La structure de contrôle de flux match
Rust a une structure de contrôle de flux très puissante appelée match
qui vous
permet de comparer une valeur avec une série de motifs et d'exécuter du code en
fonction du motif qui correspond. Les motifs peuvent être constitués de valeurs
littérales, de noms de variables, de jokers, parmi tant d'autres ; le
chapitre 18 va couvrir tous les différents types de motifs et ce qu'ils font. Ce
qui fait la puissance de match
est l'expressivité des motifs et le fait que le
compilateur vérifie que tous les cas possibles sont bien gérés.
Considérez l'expression match
comme une machine à trier les pièces de
monnaie : les pièces descendent le long d'une piste avec des trous de tailles
différentes, et chaque pièce tombe dans le premier trou à sa taille qu'elle
rencontre. De manière similaire, les valeurs parcourent tous les motifs dans un
match
, et au premier motif auquel la valeur “correspond”, la valeur va
descendre dans le bloc de code correspondant afin d'être utilisée pendant son
exécution. En parlant des pièces, utilisons-les avec un exemple qui utilise
match
! Nous pouvons écrire une fonction qui prend en paramètre une pièce
inconnue des États-Unis d'Amérique et qui peut, de la même manière qu'une
machine à trier, déterminer quelle pièce c'est et retourner sa valeur en
centimes, comme ci-dessous dans l'encart 6-3.
enum PieceUs { Penny, Nickel, Dime, Quarter, } fn valeur_en_centimes(piece: PieceUs) -> u8 { match piece { PieceUs::Penny => 1, PieceUs::Nickel => 5, PieceUs::Dime => 10, PieceUs::Quarter => 25, } } fn main() {}
Décomposons le match
dans la fonction valeur_en_centimes
. En premier lieu,
nous utilisons le mot-clé match
suivi par une expression, qui dans notre cas
est la valeur de piece
. Cela ressemble beaucoup à une expression utilisée avec
if
, mais il y a une grosse différence : avec if
, l'expression doit retourner
une valeur booléenne, mais ici, elle retourne n'importe quel type. Dans cet
exemple, piece
est de type PieceUs
, qui est l'énumération que nous avons
définie à la première ligne.
Ensuite, nous avons les branches du match
. Une branche a deux parties : un
motif et du code. La première branche a ici pour motif la valeur
PieceUs::Penny
et ensuite l'opérateur =>
qui sépare le motif et le code à
exécuter. Le code dans ce cas est uniquement la valeur 1
. Chaque branche est
séparée de la suivante par une virgule.
Lorsqu'une expression match
est exécutée, elle compare la valeur de piece
avec le motif de chaque branche, dans l'ordre. Si un motif correspond à la
valeur, le code correspondant à ce motif est alors exécuté. Si ce motif ne
correspond pas à la valeur, l'exécution passe à la prochaine branche, un peu
comme dans une machine de tri de pièces. Nous pouvons avoir autant de branches
que nécessaire : dans l'encart 6-3, notre match
a quatre branches.
Le code correspondant à chaque branche est une expression, et la valeur qui
résulte de l'expression dans la branche correspondante est la valeur qui sera
retournée par l'expression match
.
Habituellement, nous n'utilisons pas les accolades si le code de la branche
correspondante est court, comme c'est le cas dans l'encart 6-3 où chaque
branche retourne simplement une valeur. Si vous voulez exécuter plusieurs
lignes de code dans une branche d'un match
, vous devez utiliser les
accolades. Par exemple, le code suivant va afficher “Un centime
porte-bonheur !” à chaque fois que la méthode est appelée avec une valeur
PieceUs::Penny
, mais va continuer à retourner la dernière valeur du
bloc, 1
:
enum PieceUs { Penny, Nickel, Dime, Quarter, } fn valeur_en_centimes(piece: PieceUs) -> u8 { match piece { PieceUs::Penny => { println!("Un centime porte-bonheur !"); 1 } PieceUs::Nickel => 5, PieceUs::Dime => 10, PieceUs::Quarter => 25, } } fn main() {}
Des motifs reliés à des valeurs
Une autre fonctionnalité intéressante des branches de match
est qu'elles
peuvent se lier aux valeurs qui correspondent au motif. C'est ainsi que nous
pouvons extraire des valeurs d'une variante d'énumération.
En guise d'exemple, changeons une de nos variantes d'énumération pour stocker
une donnée à l'intérieur. Entre 1999 et 2008, les États-Unis d'Amérique ont
frappé un côté des quarters (pièces de 25 centimes) avec des dessins
différents pour chacun des 50 États. Les autres pièces n'ont pas eu de dessins
d'États, donc seul le quarter a cette valeur en plus. Nous pouvons ajouter
cette information à notre enum
en changeant la variante Quarter
pour y
ajouter une valeur EtatUs
qui y sera stockée à l'intérieur, comme nous
l'avons fait dans l'encart 6-4.
#[derive(Debug)] // pour pouvoir afficher l'État enum EtatUs { Alabama, Alaska, // -- partie masquée ici -- } enum PieceUs { Penny, Nickel, Dime, Quarter(EtatUs), } fn main() {}
Imaginons qu'un de vos amis essaye de collectionner tous les quarters des 50 États. Pendant que nous trions notre monnaie en vrac par type de pièce, nous mentionnerons aussi le nom de l'État correspondant à chaque quarter de sorte que si notre ami ne l'a pas, il puisse l'ajouter à sa collection.
Dans l'expression match
de ce code, nous avons ajouté une variable etat
au
motif qui correspond à la variante PieceUs::Quarter
. Quand on aura une
correspondance PieceUs::Quarter
, la variable etat
sera liée à la valeur de
l'État de cette pièce. Ensuite, nous pourrons utiliser etat
dans le code de
cette branche, comme ceci :
#[derive(Debug)] enum EtatUs { Alabama, Alaska, // -- partie masquée ici -- } enum PieceUs { Penny, Nickel, Dime, Quarter(EtatUs), } fn valeur_en_centimes(piece: PieceUs) -> u8 { match piece { PieceUs::Penny => 1, PieceUs::Nickel => 5, PieceUs::Dime => 10, PieceUs::Quarter(etat) => { println!("Il s'agit d'un quarter de l'État de {:?} !", etat); 25 }, } } fn main() { valeur_en_centimes(PieceUs::Quarter(EtatUs::Alaska)); }
Si nous appelons valeur_en_centimes(PieceUs::Quarter(EtatUs::Alaska))
, piece
vaudra PieceUs::Quarter(EtatUs::Alaska)
. Quand nous comparons cette valeur
avec toutes les branches du match
, aucune d'entre elles ne correspondra
jusqu'à ce qu'on arrive à PieceUs::Quarter(etat)
. À partir de ce moment, la
variable etat
aura la valeur EtatUs::Alaska
. Nous pouvons alors utiliser
cette variable dans l'expression println!
, ce qui nous permet d'afficher la
valeur de l'État à l'intérieur de la variante Quarter
de l'énumération
PieceUs
.
Utiliser match
avec Option<T>
Dans la section précédente, nous voulions obtenir la valeur interne T
dans le
cas de Some
lorsqu'on utilisait Option<T>
; nous pouvons aussi gérer les
Option<T>
en utilisant match
comme nous l'avons fait avec l'énumération
PieceUs
! Au lieu de comparer des pièces, nous allons comparer les variantes
de Option<T>
, mais la façon d'utiliser l'expression match
reste la même.
Disons que nous voulons écrire une fonction qui prend une Option<i32>
et qui,
s'il y a une valeur à l'intérieur, ajoute 1 à cette valeur. S'il n'y a pas de
valeur à l'intérieur, la fonction retournera la valeur None
et ne va rien
faire de plus.
Cette fonction est très facile à écrire, grâce à match
, et ressemblera à
l'encart 6-5.
fn main() { fn plus_un(x: Option<i32>) -> Option<i32> { match x { None => None, Some(i) => Some(i + 1), } } let cinq = Some(5); let six = plus_un(cinq); let none = plus_un(None); }
Examinons la première exécution de plus_un
en détail. Lorsque nous appelons
plus_un(cinq)
, la variable x
dans le corps de plus_un
aura la valeur
Some(5)
. Ensuite, nous comparons cela à chaque branche du match
.
fn main() {
fn plus_un(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let cinq = Some(5);
let six = plus_un(cinq);
let none = plus_un(None);
}
La valeur Some(5)
ne correspond pas au motif None
, donc nous continuons à la
branche suivante.
fn main() {
fn plus_un(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let cinq = Some(5);
let six = plus_un(cinq);
let none = plus_un(None);
}
Est-ce que Some(5)
correspond au motif Some(i)
? Bien sûr ! Nous avons la
même variante. Le i
va prendre la valeur contenue dans le Some
, donc i
prend la valeur 5
. Le code dans la branche du match
est exécuté, donc nous
ajoutons 1 à la valeur de i
et nous créons une nouvelle valeur Some
avec
notre résultat 6
à l'intérieur.
Maintenant, regardons le second appel à plus_un
dans l'encart 6-5, où x
vaut
None
. Nous entrons dans le match
et nous le comparons à la première branche.
fn main() {
fn plus_un(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let cinq = Some(5);
let six = plus_un(cinq);
let none = plus_un(None);
}
Cela correspond ! Il n'y a pas de valeur à additionner, donc le programme
s'arrête et retourne la valeur None
qui est dans le côté droit du =>
. Comme
la première branche correspond, les autres branches ne sont pas comparées.
La combinaison de match
et des énumérations est utile dans de nombreuses
situations. Vous allez revoir de nombreuses fois ce schéma dans du code Rust :
utiliser match
sur une énumération, récupérer la valeur qu'elle renferme, et
exécuter du code en fonction de sa valeur. C'est un peu délicat au début, mais
une fois que vous vous y êtes habitué, vous regretterez de ne pas l'avoir dans
les autres langages. Cela devient toujours l'outil préféré de ses utilisateurs.
Les match
sont toujours exhaustifs
Il y a un autre point de match
que nous devons aborder. Examinez cette version
de notre fonction plus_un
qui a un bogue et ne va pas se compiler :
fn main() {
fn plus_un(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
}
}
let cinq = Some(5);
let six = plus_un(cinq);
let none = plus_un(None);
}
Nous n'avons pas géré le cas du None
, donc ce code va générer un bogue.
Heureusement, c'est un bogue que Rust sait gérer. Si nous essayons de compiler
ce code, nous allons obtenir cette erreur :
$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
--> src/main.rs:3:15
|
3 | match x {
| ^ pattern `None` not covered
|
= help: ensure that all possible cases are being handled, possibly by adding wildcards or more match arms
= note: the matched value is of type `Option<i32>`
For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` due to previous error
Rust sait que nous n'avons pas couvert toutes les possibilités et sait même quel
motif nous avons oublié ! Les match
de Rust sont exhaustifs : nous devons
traiter toutes les possibilités afin que le code soit valide. C'est notamment le
cas avec Option<T>
: quand Rust nous empêche d'oublier de gérer explicitement
le cas de None
, il nous protège d'une situation où nous supposons que nous
avons une valeur alors que nous pourrions avoir null, ce qui rend impossible
l'erreur à un milliard de dollars que nous avons vue précédemment.
Les motifs génériques et le motif _
En utilisant les énumérations, nous pouvons aussi appliquer des actions
spéciales pour certaines valeurs précises, mais une action par défaut pour
toutes les autres valeurs. Imaginons que nous implémentons un jeu dans lequel,
si vous obtenez une valeur de 3 sur un lancé de dé, votre joueur ne se déplace
pas, mais à la place il obtient un nouveau chapeau fataisie. Si vous obtenez
un 7, votre joueur perd son chapeau fantaisie. Pour toutes les autres valeurs,
votre joueur se déplace de ce nombre de cases sur le plateau du jeu. Voici un
match
qui implémente cette logique, avec le résultat du lancé de dé codé en
dur plutôt qu'issu d'une génération aléatoire, et toute la logique des autres
fonctions sont des corps vides car leur implémentation n'est pas le sujet de
cet exemple :
fn main() { let jete_de_de = 9; match jete_de_de { 3 => ajouter_chapeau_fantaisie(), 7 => enleve_chapeau_fantaisie(), autre => deplace_joueur(autre), } fn ajouter_chapeau_fantaisie() {} fn enleve_chapeau_fantaisie() {} fn deplace_joueur(nombre_cases: u8) {} }
Dans les deux premières branches, les motifs sont les valeurs litérales 3 et 7.
La dernière branche couvre toutes les autres valeurs possibles, le motif est la
variable autre
. Le code qui s'exécute pour la branche autre
utilise la
variable en la passant dans la fonction deplacer_joueur
.
Ce code se compile, même si nous n'avons pas listé toutes les valeurs possibles
qu'un u8
puisse avoir, car le dernier motif va correspondre à toutes les
valeurs qui ne sont pas spécifiquement listés. Ce motif générique répond à la
condition qu'un match
doive être exhaustif. Notez que nous devons placer la
branche avec le motif générique en tout dernier, car les motifs sont évalués
dans l'ordre. Rust va nous prévenir si nous ajoutons des branches après un motif
générique car toutes ces autres branches ne seront jamais vérifiées !
Rust a aussi un motif que nous pouvons utiliser lorsque nous n'avons pas besoin
d'utiliser la valeur dans le motif générique : _
, qui est un motif spécial
qui vérifie n'importe quelle valeur et ne récupère pas cette valeur. Ceci
indique à Rust que nous n'allons pas utiliser la valeur, donc Rust ne va pas
nous prévenir qu'il y a une variable non utilisée.
Changeons les règles du jeu pour que si nous obtenions autre chose qu'un 3 ou
un 7, nous jetions à nouveau le dé. Nous n'avons pas besoin d'utiliser la valeur
dans ce cas, donc nous pouvons changer notre code pour utiliser _
au lieu de
la variable autre
:
fn main() { let jete_de_de = 9; match jete_de_de { 3 => ajouter_chapeau_fantaisie(), 7 => enleve_chapeau_fantaisie(), _ => relancer(), } fn ajouter_chapeau_fantaisie() {} fn enleve_chapeau_fantaisie() {} fn relancer() {} }
Cet exemple répond bien aux critères d'exhaustivité car nous ignorons explicitement toutes les autres valeurs dans la dernière branche ; nous n'avons rien oublié.
Si nous changeons à nouveau les règles du jeu, afin que rien se passe si vous
obtenez autre chose qu'un 3 ou un 7, nous pouvons exprimer cela en utilisant la
valeur unité (le type tuple vide que nous avons cité dans une section
précédente) dans le code de la branche _
:
fn main() { let jete_de_de = 9; match jete_de_de { 3 => ajouter_chapeau_fantaisie(), 7 => enleve_chapeau_fantaisie(), _ => (), } fn ajouter_chapeau_fantaisie() {} fn enleve_chapeau_fantaisie() {} }
Ici, nous indiquons explicitement à Rust que nous n'allons pas utiliser d'autres valeurs qui ne correspondent pas à un motif des branches antérieures, et nous ne voulons lancer aucun code dans ce cas.
Il existe aussi d'autres motifs que nous allons voir dans le
chapitre 18. Pour l'instant, nous allons voir
l'autre syntaxe if let
, qui peut se rendre utile dans des cas où l'expression
match
est trop verbeuse.