Filtrage par motifs#
Le filtrage par motifs (pattern matching) est l’un des mécanismes les plus expressifs de Rust. Il permet de comparer une valeur à une série de motifs structurels et d’en extraire les composantes en une seule opération, dans une syntaxe concise et vérifiée à la compilation.
L’expression match en profondeur#
Définition 53 (Exhaustivité du match)
L’expression match compare une valeur — le scrutin — à une liste ordonnée de bras. Chaque bras associe un motif à une expression. Le compilateur garantit l”exhaustivité : tous les cas possibles doivent être couverts. Si un bras manque, le programme ne compile pas. Cette vérification statique élimine toute une classe d’erreurs à l’exécution.
enum Direction { Nord, Sud, Est, Ouest }
let cap = Direction::Est;
let texte = match cap {
Direction::Nord => "vers le nord",
Direction::Sud => "vers le sud",
Direction::Est => "vers l'est",
Direction::Ouest => "vers l'ouest",
};
println!("Cap : {texte}");
Cap : vers l'est
Si l’on retire un bras, le compilateur signale une erreur. C’est cette garantie qui rend le match plus sur qu’une chaîne de if/else.
Types de motifs#
Littéraux, variables et joker#
Les motifs les plus simples sont les littéraux, les variables qui capturent la valeur, et le joker _ qui accepte toute valeur sans la lier.
Exemple 54 (Motifs de base)
Voir le code ci-dessous.
let n = 7;
match n {
0 => println!("zéro"),
1 => println!("un"),
x => println!("autre valeur : {x}"),
}
autre valeur : 7
Le motif x capture la valeur de n dans la variable x. Le joker _ fait de même sans créer de liaison.
Motifs composés avec |#
L’opérateur | permet de regrouper plusieurs motifs dans un même bras.
let code = 403;
let categorie = match code {
200 | 201 | 204 => "succès",
301 | 302 => "redirection",
400 | 403 | 404 => "erreur client",
500 | 502 | 503 => "erreur serveur",
_ => "inconnu",
};
println!("{code} -> {categorie}");
403 -> erreur client
Plages avec ..=#
Les motifs de plage inclusive ..= filtrent un intervalle de valeurs. Ils sont disponibles pour les entiers et les caractères.
Exemple 55 (Plages dans les motifs)
Voir le code ci-dessous.
let note = 14;
let mention = match note {
0..=7 => "insuffisant",
8..=9 => "rattrapable",
10..=11 => "passable",
12..=13 => "assez bien",
14..=15 => "bien",
16..=20 => "très bien",
_ => "hors barème",
};
println!("Note {note}/20 : {mention}");
Note 14/20 : bien
let c = 'G';
let categorie = match c {
'a'..='z' => "minuscule",
'A'..='Z' => "majuscule",
'0'..='9' => "chiffre",
_ => "autre",
};
println!("'{c}' est un caractère {categorie}");
'G' est un caractère majuscule
Destructuration#
Le filtrage excelle lorsqu’il s’agit de décomposer des valeurs composites.
Tuples#
let point = (3, -2);
match point {
(0, 0) => println!("origine"),
(x, 0) => println!("sur l'axe x en {x}"),
(0, y) => println!("sur l'axe y en {y}"),
(x, y) => println!("point ({x}, {y})"),
}
point (3, -2)
Structures#
Exemple 56 (Destructuration de structures)
Voir le code ci-dessous.
struct Vecteur { x: f64, y: f64 }
let v = Vecteur { x: 0.0, y: 5.0 };
match v {
Vecteur { x: 0.0, y: 0.0 } => println!("vecteur nul"),
Vecteur { x: 0.0, y } => println!("vertical, composante y = {y}"),
Vecteur { x, y: 0.0 } => println!("horizontal, composante x = {x}"),
Vecteur { x, y } => println!("vecteur ({x}, {y})"),
}
vertical, composante y = 5
Énumérations#
Chaque variante d’une énumération peut porter des données que le motif extrait directement.
enum Forme {
Cercle(f64),
Rectangle(f64, f64),
Triangle { base: f64, hauteur: f64 },
}
let f = Forme::Triangle { base: 6.0, hauteur: 4.0 };
let aire = match f {
Forme::Cercle(r) => std::f64::consts::PI * r * r,
Forme::Rectangle(l, h) => l * h,
Forme::Triangle { base, hauteur } => 0.5 * base * hauteur,
};
println!("Aire = {aire}");
Aire = 12
Gardes de motifs#
Définition 54 (Garde de motif)
Une garde de motif est une condition booléenne introduite par if après le motif d’un bras match. Le bras n’est sélectionné que si le motif correspond et la garde est vraie. Les gardes permettent d’exprimer des conditions qui dépassent le pouvoir expressif des motifs seuls.
Exemple 57 (Gardes dans un match)
Voir le code ci-dessous.
let temperature = 37;
let diagnostic = match temperature {
t if t < 0 => "gelé",
t if t < 36 => "hypothermie",
36..=37 => "normal",
t if t <= 40 => "fièvre",
_ => "hyperthermie sévère",
};
println!("{temperature}°C : {diagnostic}");
37°C : normal
Remarque 40
Le compilateur ne tient pas compte des gardes pour vérifier l’exhaustivité. Même si les gardes couvrent logiquement tous les cas, un bras par défaut (_) ou un motif variable reste nécessaire.
Liaison avec @#
Définition 55 (Opérateur de liaison @)
L’opérateur @ (at) lie une variable à une valeur tout en testant cette valeur contre un motif. La syntaxe nom @ motif capture la valeur dans nom si elle correspond au motif.
Exemple 58 (Liaison avec @)
Voir le code ci-dessous.
let n = 15;
match n {
e @ 1..=9 => println!("{e} est un chiffre"),
d @ 10..=99 => println!("{d} est un nombre à deux chiffres"),
c @ 100..=999 => println!("{c} est un nombre à trois chiffres"),
autre => println!("{autre} est un grand nombre"),
}
15 est un nombre à deux chiffres
L’opérateur @ est particulièrement utile avec les énumérations.
enum Message { Texte(String), Code(u32) }
let msg = Message::Code(404);
match msg {
Message::Code(c @ 400..=499) => println!("erreur client : {c}"),
Message::Code(c @ 500..=599) => println!("erreur serveur : {c}"),
Message::Code(c) => println!("code : {c}"),
Message::Texte(t) => println!("texte : {t}"),
}
erreur client : 404
if let et while let#
Définition 56 (if let et while let)
Les constructions if let et while let sont des formes concises du match lorsque l’on ne s’intéresse qu’à un seul motif, les autres cas étant ignorés ou traités par un else.
if let#
Lorsque seul un cas nous intéresse, if let évite la verbosité d’un match complet.
let valeur: Option<i32> = Some(42);
if let Some(n) = valeur {
println!("valeur présente : {n}");
} else {
println!("aucune valeur");
}
valeur présente : 42
while let#
La boucle while let itère tant que le motif correspond. Elle est idiomatique pour dépiler une collection.
Exemple 59 (while let avec une pile)
Voir le code ci-dessous.
let mut pile = vec![1, 2, 3, 4, 5];
while let Some(sommet) = pile.pop() {
println!("dépilé : {sommet}");
}
println!("pile vide : {:?}", pile);
dépilé : 5
dépilé : 4
dépilé : 3
dépilé : 2
dépilé : 1
pile vide : []
Motifs irréfutables et réfutables#
Définition 57 (Motif irréfutable et réfutable)
Un motif irréfutable correspond à toute valeur possible du type concerné. Exemples : une variable x, le joker _, un tuple (a, b). Un motif réfutable peut échouer : un littéral 42, un variant Some(x), une plage 1..=10. Rust impose des motifs irréfutables dans les contextes let, les paramètres de fonctions et les boucles for. Les contextes match, if let et while let acceptent les motifs réfutables.
// Irréfutable : toujours valide dans un let
let (a, b) = (1, 2);
println!("a = {a}, b = {b}");
// Réfutable : nécessite if let ou match
let option: Option<i32> = Some(10);
if let Some(v) = option {
println!("v = {v}");
}
a = 1, b = 2
v = 10
Remarque 41
Utiliser un motif réfutable dans un let provoque une erreur de compilation. Inversement, un motif irréfutable dans un if let provoque un avertissement.
Motifs imbriqués et patterns avancés#
Les motifs peuvent être composés et imbriqués pour filtrer des structures complexes.
Exemple 60 (Motifs imbriqués)
Voir le code ci-dessous.
enum Expr {
Nombre(f64),
Add(Box<Expr>, Box<Expr>),
Neg(Box<Expr>),
}
fn eval(expr: &Expr) -> f64 {
match expr {
Expr::Nombre(n) => *n,
Expr::Add(a, b) => eval(a) + eval(b),
Expr::Neg(inner) => -eval(inner),
}
}
let e = Expr::Add(
Box::new(Expr::Nombre(3.0)),
Box::new(Expr::Neg(Box::new(Expr::Nombre(1.0)))),
);
println!("3 + (-1) = {}", eval(&e));
3 + (-1) = 2
Utilisation idiomatique#
Remarque 42
Le filtrage par motifs imprègne tout le code Rust idiomatique. Voici les principales conventions :
Préférer
matchà une cascade deif/elselorsque la valeur testée possède une structure décomposable.Utiliser
if letquand un seul cas est pertinent ; éviter unmatchà deux bras dont l’un est_ => ().Exploiter la destructuration pour accéder aux champs sans recourir à la notation pointée.
Laisser le compilateur vérifier l’exhaustivité : ne pas ajouter de bras
_par réflexe lorsque tous les cas sont énumérables, afin de recevoir une erreur si un nouveau variant est ajouté.
fn diviser(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 { Err(String::from("division par zéro")) }
else { Ok(a / b) }
}
// match exhaustif sur Result
match diviser(10.0, 3.0) {
Ok(resultat) => println!("10 / 3 = {resultat:.4}"),
Err(e) => println!("Erreur : {e}"),
}
// if let pour n'exploiter que le cas Ok
if let Ok(r) = diviser(22.0, 7.0) {
println!("22 / 7 = {r:.6}");
}
10 / 3 = 3.3333
22 / 7 = 3.142857