Le pattern matching en Rust
Sur la page Match, vous avez appris les bases : les branches, _, le regroupement de valeurs avec | et les plages. Ce n’est qu’un début. Un pattern (motif) en Rust peut aussi regarder à l’intérieur d’une valeur, en extraire les morceaux dont vous avez besoin et vérifier des conditions supplémentaires, le tout en une seule ligne. C’est ce qu’on appelle le pattern matching (filtrage par motif).
Imaginez un motif comme un emporte-pièce. Vous l’appuyez sur une valeur. Si la forme correspond, Rust vous donne les morceaux qui sont à l’intérieur. Sinon, Rust essaie la branche suivante. Une fois que vous aurez compris l’idée, vous utiliserez des motifs partout : dans match, dans let, dans les paramètres de fonction et dans if let.
Les gardes : ajouter un if
Section intitulée « Les gardes : ajouter un if »Parfois, la forme ne suffit pas et il faut une vérification en plus. Une garde (match guard) est une condition if écrite après le motif. La branche ne s’exécute que si le motif correspond et que la condition est vraie.
fn main() { let temperatures = [-5, 0, 18, 35];
for t in temperatures { let feeling = match t { t if t < 0 => "glacial", 0 => "pile zéro", t if t < 25 => "agréable", _ => "chaud", }; println!("{t} °C : {feeling}"); }}Sortie :
-5 °C : glacial0 °C : pile zéro18 °C : agréable35 °C : chaudRust teste les branches de haut en bas, donc l’ordre compte. 18 échoue à la première garde, n’est pas 0, et passe t < 25 : il est donc « agréable ».
Déstructurer des tuples et des structs
Section intitulée « Déstructurer des tuples et des structs »Déstructurer une valeur, c’est la découper en ses différentes parties. Pour un tuple, écrivez le motif avec la même forme que le tuple. Pour une struct, nommez les champs qui vous intéressent.
struct Point { x: i32, y: i32,}
fn main() { let pair = (3, -7); match pair { (0, 0) => println!("l'origine"), (x, 0) => println!("sur l'axe x en {x}"), (0, y) => println!("sur l'axe y en {y}"), (x, y) => println!("ailleurs : ({x}, {y})"), }
let p = Point { x: 0, y: 5 }; match p { Point { x: 0, y } => println!("sur l'axe y, y = {y}"), Point { x, y: 0 } => println!("sur l'axe x, x = {x}"), Point { x, y } => println!("en ({x}, {y})"), }}Sortie :
ailleurs : (3, -7)sur l'axe y, y = 5Remarquez le mélange. 0 est une valeur fixe qui doit correspondre exactement. x et y sont de nouvelles variables qui récupèrent ce qui se trouve à cet endroit. Point { x, y } est un raccourci pour Point { x: x, y: y }.
Déstructurer des enums qui portent des données
Section intitulée « Déstructurer des enums qui portent des données »C’est là que les motifs brillent vraiment. Une variante d’enum peut transporter des données, et un motif peut extraire ces données tout en vérifiant de quelle variante il s’agit.
enum Shape { Circle { radius: f64 }, Rectangle { width: f64, height: f64 }, Square(f64),}
fn area(shape: &Shape) -> f64 { match shape { Shape::Circle { radius } => std::f64::consts::PI * radius * radius, Shape::Rectangle { width, height } => width * height, Shape::Square(side) => side * side, }}
fn main() { let shapes = [ Shape::Circle { radius: 2.0 }, Shape::Rectangle { width: 3.0, height: 4.5 }, Shape::Square(1.5), ]; for shape in &shapes { println!("aire = {:.2}", area(shape)); }}Sortie :
aire = 12.57aire = 13.50aire = 2.25Impossible d’accéder à radius sans avoir d’abord prouvé que la valeur est un Circle. C’est là que se trouve le gain de sécurité : le compilateur ne vous laisse pas lire les données de la mauvaise variante.
Motifs imbriqués et liaisons @
Section intitulée « Motifs imbriqués et liaisons @ »Les motifs peuvent s’emboîter les uns dans les autres, aussi profondément que vous le voulez. Ici, une enum Command contient une enum Direction, et une seule branche fait correspondre les deux niveaux à la fois.
Une liaison @ (binding) vous permet de tester une valeur avec un motif et de la garder dans une variable. n @ 1..=9 signifie « vérifie qu’elle est entre 1 et 9, et appelle-la n ».
enum Direction { Up, Down,}
enum Command { Move(Direction, u32), Say(String), Quit,}
fn run(cmd: Command) { match cmd { Command::Move(Direction::Up, steps @ 1..=9) => println!("monte de {steps} (petit pas)"), Command::Move(Direction::Up, steps) => println!("monte de {steps} (grand saut !)"), Command::Move(Direction::Down, steps) => println!("descend de {steps}"), Command::Say(text) => println!("dit : {text}"), Command::Quit => println!("au revoir"), }}
fn main() { run(Command::Move(Direction::Up, 3)); run(Command::Move(Direction::Up, 50)); run(Command::Move(Direction::Down, 2)); run(Command::Say(String::from("bonjour"))); run(Command::Quit);}Sortie :
monte de 3 (petit pas)monte de 50 (grand saut !)descend de 2dit : bonjourau revoirSans @, une plage comme 1..=9 vérifie la valeur mais la jette ensuite. Avec @, vous pouvez la garder.
Ignorer des parties avec _ et ..
Section intitulée « Ignorer des parties avec _ et .. »Souvent, seuls certains morceaux vous intéressent. Utilisez _ pour ignorer une valeur, et .. pour ignorer « tout le reste ».
struct Player { name: String, level: u32, gold: u32,}
fn main() { let rgb = (255, 128, 0); let (red, _, _) = rgb; println!("rouge = {red}");
let numbers = (1, 2, 3, 4, 5); let (first, .., last) = numbers; println!("premier = {first}, dernier = {last}");
let hero = Player { name: String::from("Mira"), level: 7, gold: 120 }; let Player { name, level, .. } = hero; println!("{name} est au niveau {level}");}Sortie :
rouge = 255premier = 1, dernier = 5Mira est au niveau 7.. ne peut apparaître qu’une seule fois dans un motif. (first, .., last) est valide, mais pas (.., middle, ..) : Rust ne saurait pas de quelle valeur vous parlez.
Les motifs dans let et dans les paramètres de fonction
Section intitulée « Les motifs dans let et dans les paramètres de fonction »Chaque let est en réalité un motif. Chaque paramètre de fonction aussi. Vous pouvez donc déstructurer une valeur à l’endroit même où elle arrive.
fn print_coords(&(x, y): &(i32, i32)) { println!("x : {x}, y : {y}");}
fn main() { let (width, height) = (1920, 1080); println!("{width}x{height}");
print_coords(&(4, -2));
let points = vec![(1, 2), (3, 4), (5, 6)]; let sums: Vec<i32> = points.iter().map(|&(a, b)| a + b).collect(); println!("{:?}", sums);}Sortie :
1920x1080x : 4, y : -2[3, 7, 11]La closure |&(a, b)| a + b utilise la même idée. Chaque élément est un &(i32, i32), et le motif le déballe directement dans a et b. Vous verrez souvent cela avec les itérateurs.
matches! et let-else
Section intitulée « matches! et let-else »Deux outils pratiques vous évitent d’écrire un match complet.
matches!(value, pattern) renvoie true ou false. C’est parfait dans un if ou un .filter() quand vous voulez seulement une réponse oui/non. Les gardes fonctionnent aussi ici.
let-else gère le cas « déballe ceci, ou bien sors tout de suite ». Écrivez let MOTIF = valeur else { ... };. Si le motif correspond, les variables sont disponibles pour le reste de la fonction. Sinon, le bloc else s’exécute, et il doit sortir : avec return, break, continue ou une panic.
fn parse_age(input: &str) -> Option<u32> { let Ok(age) = input.parse::<u32>() else { println!("'{input}' n'est pas un nombre"); return None; }; Some(age)}
fn main() { let grade = 'B'; println!("{}", matches!(grade, 'A' | 'B'));
let scores = [45, 72, 90, 38, 66]; let passed = scores.iter().filter(|s| matches!(s, 50..=100)).count(); println!("{passed} réussis");
println!("{:?}", parse_age("31")); println!("{:?}", parse_age("trente"));}Sortie :
true3 réussisSome(31)'trente' n'est pas un nombreNonelet-else garde le « chemin heureux » (le cas normal) bien à plat. Au lieu d’imbriquer votre code dans un if let ... { }, vous traitez d’abord le mauvais cas, puis vous continuez. Utilisez-le avec des valeurs Option et Result.
Erreurs courantes
Section intitulée « Erreurs courantes »Les gardes ne comptent pas pour « tous les cas sont couverts ». Le compilateur ne peut pas raisonner sur vos conditions if, donc ce code échoue, même si les deux gardes couvrent tous les nombres :
fn main() { let n: i32 = 7; match n { x if x < 0 => println!("négatif"), x if x >= 0 => println!("zéro ou positif"), }}error[E0004]: non-exhaustive patterns: `i32::MIN..=i32::MAX` not covered --> src/main.rs:3:11 |3 | match n { | ^ pattern `i32::MIN..=i32::MAX` not covered | = note: the matched value is of type `i32` = note: match arms with guards don't count towards exhaustivityLe compilateur dit que les motifs ne sont pas exhaustifs : toute la plage des i32 n’est pas couverte, car les branches avec garde ne comptent pas. La solution : faites de la dernière branche un simple attrape-tout, par exemple _ => println!("zéro ou positif").
Un nom de variable dans un motif crée une nouvelle variable, ce n’est pas une comparaison. Ce code semble vérifier guess == expected, mais ce n’est pas le cas :
fn main() { let expected = 3; let guess = 5; match guess { expected => println!("Correct !"), _ => println!("Faux"), }}Il compile et affiche Correct !, car expected dans la branche crée simplement une nouvelle variable qui attrape n’importe quelle valeur. Rust vous prévient que la branche _ ne peut jamais être atteinte :
warning: unreachable pattern --> src/main.rs:6:9 |5 | expected => println!("Correct !"), | -------- matches any value6 | _ => println!("Faux"), | ^ no value can reach thisLa solution est une garde : g if g == expected => println!("Correct !").
Exercices
Section intitulée « Exercices »1. FizzBuzz avec un tuple
Section intitulée « 1. FizzBuzz avec un tuple »Affichez les nombres de 9 à 15, mais écrivez Fizz pour les multiples de 3, Buzz pour les multiples de 5, et FizzBuzz pour les multiples des deux. Faites-le avec un seul match sur un tuple, au lieu d’une chaîne de if. Indice : faites le match sur (n % 3, n % 5). Un reste de 0 signifie « divisible », et _ signifie « peu importe ».
Voir la solution
fn main() { for n in 9..=15 { match (n % 3, n % 5) { (0, 0) => println!("FizzBuzz"), (0, _) => println!("Fizz"), (_, 0) => println!("Buzz"), _ => println!("{n}"), } }}Sortie :
FizzBuzz11Fizz1314FizzBuzz(0, 0) doit venir en premier. Si (0, _) était en tête, 15 afficherait Fizz.
2. Transactions bancaires
Section intitulée « 2. Transactions bancaires »Définissez enum Transaction { Deposit(u32), Withdraw(u32), Fee }. Initialisez un solde balance à 100, puis appliquez cette liste dans l’ordre : dépôt de 50, retrait de 30, retrait de 500, frais. Les frais coûtent 5. Un retrait plus grand que le solde doit être refusé avec un message. Affichez le solde final. Indice : utilisez une garde, Transaction::Withdraw(amount) if amount <= balance, suivie d’une deuxième branche Withdraw pour le cas refusé.
Voir la solution
enum Transaction { Deposit(u32), Withdraw(u32), Fee,}
fn main() { let mut balance: u32 = 100; let history = [ Transaction::Deposit(50), Transaction::Withdraw(30), Transaction::Withdraw(500), Transaction::Fee, ];
for t in history { match t { Transaction::Deposit(amount) => balance += amount, Transaction::Withdraw(amount) if amount <= balance => balance -= amount, Transaction::Withdraw(amount) => println!("refusé : impossible de retirer {amount}"), Transaction::Fee => balance -= 5, } }
println!("solde = {balance}");}Sortie :
refusé : impossible de retirer 500solde = 115La deuxième branche Withdraw n’a pas de garde : elle attrape donc tous les retraits que la première a refusés. Sans elle, le match ne compilerait pas, car les gardes ne comptent pas quand Rust vérifie que tous les cas sont couverts.
Récapitulatif
Section intitulée « Récapitulatif »- Une garde (
motif if condition) ajoute une vérification supplémentaire à une branche. - Les motifs peuvent déstructurer des tuples, des structs et des variantes d’enum, et ils peuvent s’imbriquer.
nom @ motifteste une valeur et la garde dans une variable en même temps._ignore une valeur ;..ignore le reste. Les motifs fonctionnent aussi danslet, dans les paramètres de fonction et dans les closures.matches!donne rapidement untrue/false; let-else déballe une valeur ou sort tout de suite.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Que fait n @ 1..=9 dans une branche de match ?
2. Avec let-else, que doit faire le bloc else ?
3. Que donne let (first, .., last) = (1, 2, 3, 4, 5); ?
4. Pourquoi un match dont toutes les branches ont une garde ne compile-t-il souvent pas ?
Score : 0 / 4
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Patterns and Matching — The Rust Book (en anglais) : le chapitre officiel sur tous les endroits où l’on peut utiliser des motifs, et sur les cas où ils peuvent échouer.
- Pattern Syntax — The Rust Book (en anglais) : un tour de toutes les formes de motifs, des plages et de la déstructuration jusqu’aux gardes et aux liaisons
@. - Staying on the Happy Path with let…else — The Rust Book (en anglais) : un exemple détaillé et accessible de let-else.
- Destructuring — Rust by Example (en anglais) : des exemples exécutables de déstructuration de tuples, de tableaux, d’enums, de pointeurs et de structs.
- Macro
matches!— documentation std (en anglais) : la syntaxe exacte de la macro, y compris comment ajouter une garde.