Aller au contenu

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.

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 : glacial
0 °C : pile zéro
18 °C : agréable
35 °C : chaud
▶ Essayez par vous-même

Rust 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 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 = 5
▶ Essayez par vous-même

Remarquez 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 }.

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.57
aire = 13.50
aire = 2.25
▶ Essayez par vous-même

Impossible 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.

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 2
dit : bonjour
au revoir
▶ Essayez par vous-même

Sans @, une plage comme 1..=9 vérifie la valeur mais la jette ensuite. Avec @, vous pouvez la garder.

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 = 255
premier = 1, dernier = 5
Mira est au niveau 7
▶ Essayez par vous-même

.. 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 :

1920x1080
x : 4, y : -2
[3, 7, 11]
▶ Essayez par vous-même

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.

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 :

true
3 réussis
Some(31)
'trente' n'est pas un nombre
None
▶ Essayez par vous-même

let-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.

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 exhaustivity

Le 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 value
6 | _ => println!("Faux"),
| ^ no value can reach this

La solution est une garde : g if g == expected => println!("Correct !").

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 :

Fizz
Buzz
11
Fizz
13
14
FizzBuzz
▶ Essayez par vous-même

(0, 0) doit venir en premier. Si (0, _) était en tête, 15 afficherait Fizz.

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 500
solde = 115
▶ Essayez par vous-même

La 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.

  • 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 @ motif teste une valeur et la garde dans une variable en même temps.
  • _ ignore une valeur ; .. ignore le reste. Les motifs fonctionnent aussi dans let, dans les paramètres de fonction et dans les closures.
  • matches! donne rapidement un true/false ; let-else déballe une valeur ou sort tout de suite.

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