La syntaxe des motifs
In this section, we gather all the syntax that is valid in patterns and discuss why and when you might want to use each one.
Matching Literals
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!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
}
This code prints one because the value in x is 1. This syntax is useful when you want your code to take an action if it gets a particular concrete value.
Matching Named Variables
Named variables are irrefutable patterns that match any value, and we’ve used them many times in this book. However, there is a complication when you use named variables in match, if let, or while let expressions. Because each of these kinds of expressions starts a new scope, variables declared as part of a pattern inside these expressions will shadow those with the same name outside the constructs, as is the case with all variables. In Listing 19-11, we declare a variable named x with the value Some(5) and a variable y with the value 10. We then create a match expression on the value x. Look at the patterns in the match arms and println! at the end, and try to figure out what the code will print before running this code or reading further.
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(y) => println!("Matched, y = {y}"),
_ => println!("Default case, x = {x:?}"),
}
println!("at the end: x = {x:?}, y = {y}");
}
match expression with an arm that introduces a new variable which shadows an existing variable yVoyons 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.
The pattern in the second match arm introduces a new variable named y that will match any value inside a Some value. Because we’re in a new scope inside the match expression, this is a new y variable, not the y we declared at the beginning with the value 10. This new y binding will match any value inside a Some, which is what we have in x. Therefore, this new y binds to the inner value of the Some in x. That value is 5, so the expression for that arm executes and prints Matched, 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 À la fin : x = Some(5), y = 10.
To create a match expression that compares the values of the outer x and y, rather than introducing a new variable that shadows the existing y variable, we would need to use a match guard conditional instead. We’ll talk about match guards later in the “Adding Conditionals with Match Guards” section.
Matching Multiple Patterns
In match expressions, you can match multiple patterns using the | syntax, which is the pattern or operator. For example, in the following code, we match the value of x against the match arms, the first of which has an or option, meaning if the value of x matches either of the values in that arm, that arm’s code will run:
fn main() {
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
}
This code prints one or two.
Matching Ranges of Values with ..=
The ..= syntax allows us to match to an inclusive range of values. In the following code, when a pattern matches any of the values within the given range, that arm will execute:
fn main() {
let x = 5;
match x {
1..=5 => println!("one through five"),
_ => println!("something else"),
}
}
If x is 1, 2, 3, 4, or 5, the first arm will match. This syntax is more convenient for multiple match values than using the | operator to express the same idea; if we were to use |, we would have to specify 1 | 2 | 3 | 4 | 5. Specifying a range is much shorter, especially if we want to match, say, any number between 1 and 1,000!
The compiler checks that the range isn’t empty at compile time, and because the only types for which Rust can tell if a range is empty or not are char and numeric values, ranges are only allowed with numeric or char values.
Here is an example using ranges of char values:
fn main() {
let x = 'c';
match x {
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"),
}
}
Rust can tell that 'c' is within the first pattern’s range and prints early ASCII letter.
Destructuring to Break Apart Values
Nous pouvons aussi utiliser les motifs pour déstructurer les structures, les énumérations, et les tuples pour utiliser différentes parties de ces valeurs. Passons en revue chacun des cas.
Structs
Listing 19-12 shows a Point struct with two fields, x and y, that we can break apart using a pattern with a let statement.
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);
}
This code creates the variables a and b that match the values of the x and y fields of the p struct. This example shows that the names of the variables in the pattern don’t have to match the field names of the struct. However, it’s common to match the variable names to the field names to make it easier to remember which variables came from which fields. Because of this common usage, and because writing let Point { x: x, y: y } = p; contains a lot of duplication, Rust has a shorthand for patterns that match struct fields: You only need to list the name of the struct field, and the variables created from the pattern will have the same names. Listing 19-13 behaves in the same way as the code in Listing 19-12, but the variables created in the let pattern are x and y instead of a and b.
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 déstructurer 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 déstructurer les autres champs.
In Listing 19-14, we have a match expression that separates Point values into three cases: points that lie directly on the x axis (which is true when y = 0), on the y axis (x = 0), or on neither axis.
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
match p {
Point { x, y: 0 } => println!("On the x axis at {x}"),
Point { x: 0, y } => println!("On the y axis at {y}"),
Point { x, y } => {
println!("On neither axis: ({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.
In this example, the value p matches the second arm by virtue of x containing a 0, so this code will print On the y axis at 7.
Remember that a match expression stops checking arms once it has found the first matching pattern, so even though Point { x: 0, y: 0 } is on the x axis and the y axis, this code would only print On the x axis at 0.
Enums
We’ve destructured enums in this book (for example, Listing 6-5 in Chapter 6), but we haven’t yet explicitly discussed that the pattern to destructure an enum corresponds to the way the data stored within the enum is defined. As an example, in Listing 19-15, we use the Message enum from Listing 6-2 and write a match with patterns that will destructure each inner value.
enum Message {
Quitter,
Deplacer { x: i32, y: i32 },
Ecrire(String),
ChangerCouleur(i32, i32, i32),
}
fn main() {
let msg = Message::ChangeColor(0, 160, 255);
match msg {
Message::Quit => {
println!("The Quit variant has no data to destructure.");
}
Message::Move { x, y } => {
println!("Move in the x direction {x} and in the y direction {y}");
}
Message::Write(text) => {
println!("Text message: {text}");
}
Message::ChangeColor(r, g, b) => {
println!("Change color to red {r}, green {g}, and blue {b}");
}
}
}
This code will print Change color to red 0, green 160, and blue 255. Try changing the value of msg to see the code from the other arms run.
Pour les variantes d’énumération sans aucune donnée, telle que Message::Quitter, nous ne pouvons pas déstructurer de valeurs. Nous pouvons uniquement correspondre à la valeur littérale Message::Quitter et il n’y a pas de variable dans ce motif.
For struct-like enum variants, such as Message::Move, we can use a pattern similar to the pattern we specify to match structs. After the variant name, we place curly brackets and then list the fields with variables so that we break apart the pieces to use in the code for this arm. Here we use the shorthand form as we did in Listing 19-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.
Nested Structs and Enums
So far, our examples have all been matching structs or enums one level deep, but matching can work on nested items too! For example, we can refactor the code in Listing 19-15 to support RGB and HSV colors in the ChangeColor message, as shown in Listing 19-16.
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
enum Message {
Quitter,
Deplacer { x: i32, y: i32 },
Ecrire(String),
ChangeColor(Color),
}
fn main() {
let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));
match msg {
Message::ChangeColor(Color::Rgb(r, g, b)) => {
println!("Change color to red {r}, green {g}, and blue {b}");
}
Message::ChangeColor(Color::Hsv(h, s, v)) => {
println!("Change color to hue {h}, saturation {s}, value {v}");
}
_ => (),
}
}
The pattern of the first arm in the match expression matches a Message::ChangeColor enum variant that contains a Color::Rgb variant; then, the pattern binds to the three inner i32 values. The pattern of the second arm also matches a Message::ChangeColor enum variant, but the inner enum matches Color::Hsv instead. We can specify these complex conditions in one match expression, even though two enums are involved.
Structs and 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 déstructurons toutes les valeurs primitives :
fn main() {
struct Point {
x: i32,
y: i32,
}
let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
}
This code lets us break complex types into their component parts so that we can use the values we’re interested in separately.
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.
Ignoring Values in a Pattern
You’ve seen that it’s sometimes useful to ignore values in a pattern, such as in the last arm of a match, to get a catch-all that doesn’t actually do anything but does account for all remaining possible values. There are a few ways to ignore entire values or parts of values in a pattern: using the _ pattern (which you’ve seen), using the _ pattern within another pattern, using a name that starts with an underscore, or using .. to ignore remaining parts of a value. Let’s explore how and why to use each of these patterns.
An Entire Value with _
We’ve used the underscore as a wildcard pattern that will match any value but not bind to the value. This is especially useful as the last arm in a match expression, but we can also use it in any pattern, including function parameters, as shown in Listing 19-17.
fn foo(_: i32, y: i32) {
println!("This code only uses the y parameter: {y}");
}
fn main() {
foo(3, 4);
}
_ in a function signatureThis code will completely ignore the value 3 passed as the first argument, and will print This code only uses the y parameter: 4.
In most cases when you no longer need a particular function parameter, you would change the signature so that it doesn’t include the unused parameter. Ignoring a function parameter can be especially useful in cases when, for example, you’re implementing a trait when you need a certain type signature but the function body in your implementation doesn’t need one of the parameters. You then avoid getting a compiler warning about unused function parameters, as you would if you used a name instead.
Parts of a Value with a Nested _
We can also use _ inside another pattern to ignore just part of a value, for example, when we want to test for only part of a value but have no use for the other parts in the corresponding code we want to run. Listing 19-18 shows code responsible for managing a setting’s value. The business requirements are that the user should not be allowed to overwrite an existing customization of a setting but can unset the setting and give it a value if it is currently unset.
fn main() {
let mut setting_value = Some(5);
let new_setting_value = Some(10);
match (setting_value, new_setting_value) {
(Some(_), Some(_)) => {
println!("Can't overwrite an existing customized value");
}
_ => {
setting_value = new_setting_value;
}
}
println!("setting is {setting_value:?}");
}
Some variants when we don’t need to use the value inside the SomeThis code will print Can't overwrite an existing customized value and then setting is Some(5). In the first match arm, we don’t need to match on or use the values inside either Some variant, but we do need to test for the case when setting_value and new_setting_value are the Some variant. In that case, we print the reason for not changing setting_value, and it doesn’t get changed.
In all other cases (if either setting_value or new_setting_value is None) expressed by the _ pattern in the second arm, we want to allow new_setting_value to become setting_value.
We can also use underscores in multiple places within one pattern to ignore particular values. Listing 19-19 shows an example of ignoring the second and fourth values in a tuple of five items.
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {first}, {third}, {fifth}");
}
}
}
This code will print Some numbers: 2, 8, 32, and the values 4 and 16 will be ignored.
An Unused Variable by Starting Its Name with _
If you create a variable but don’t use it anywhere, Rust will usually issue a warning because an unused variable could be a bug. However, sometimes it’s useful to be able to create a variable you won’t use yet, such as when you’re prototyping or just starting a project. In this situation, you can tell Rust not to warn you about the unused variable by starting the name of the variable with an underscore. In Listing 19-20, we create two unused variables, but when we compile this code, we should only get a warning about one of them.
fn main() {
let _x = 5;
let y = 10;
}
Here, we get a warning about not using the variable y, but we don’t get a warning about not using _x.
Note that there is a subtle difference between using only _ and using a name that starts with an underscore. The syntax _x still binds the value to the variable, whereas _ doesn’t bind at all. To show a case where this distinction matters, Listing 19-21 will provide us with an error.
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{s:?}");
}
We’ll receive an error because the s value will still be moved into _s, which prevents us from using s again. However, using the underscore by itself doesn’t ever bind to the value. Listing 19-22 will compile without any errors because s doesn’t get moved into _.
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_) = s {
println!("found a string");
}
println!("{s:?}");
}
This code works just fine because we never bind s to anything; it isn’t moved.
Remaining Parts of a Value with ..
With values that have many parts, we can use the .. syntax to use specific parts and ignore the rest, avoiding the need to list underscores for each ignored value. The .. pattern ignores any parts of a value that we haven’t explicitly matched in the rest of the pattern. In Listing 19-23, we have a Point struct that holds a coordinate in three-dimensional space. In the match expression, we want to operate only on the x coordinate and ignore the values in the y and z fields.
fn main() {
struct Point {
x: i32,
y: i32,
z: i32,
}
let origin = Point { x: 0, y: 0, z: 0 };
match origin {
Point { x, .. } => println!("x is {x}"),
}
}
Point except for x by using ..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.
The syntax .. will expand to as many values as it needs to be. Listing 19-24 shows how to use .. with a tuple.
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, .., last) => {
println!("Some numbers: {first}, {last}");
}
}
}
In this code, the first and last values are matched with first and last. The .. will match and ignore everything in the middle.
However, using .. must be unambiguous. If it is unclear which values are intended for matching and which should be ignored, Rust will give us an error. Listing 19-25 shows an example of using .. ambiguously, so it will not compile.
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(.., second, ..) => {
println!("Some numbers: {second}")
},
}
}
.. in an ambiguous wayWhen we compile this example, we get this error:
$ 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` (bin "patterns") due to 1 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.
Adding Conditionals with Match Guards
A match guard is an additional if condition, specified after the pattern in a match arm, that must also match for that arm to be chosen. Match guards are useful for expressing more complex ideas than a pattern alone allows. Note, however, that they are only available in match expressions, not if let or while let expressions.
The condition can use variables created in the pattern. Listing 19-26 shows a match where the first arm has the pattern Some(x) and also has a match guard of if x % 2 == 0 (which will be true if the number is even).
fn main() {
let num = Some(4);
match num {
Some(x) if x % 2 == 0 => println!("The number {x} is even"),
Some(x) => println!("The number {x} is odd"),
None => (),
}
}
This example will print The number 4 is even. When num is compared to the pattern in the first arm, it matches because Some(4) matches Some(x). Then, the match guard checks whether the remainder of dividing x by 2 is equal to 0, and because it is, the first arm is selected.
If num had been Some(5) instead, the match guard in the first arm would have been false because the remainder of 5 divided by 2 is 1, which is not equal to 0. Rust would then go to the second arm, which would match because the second arm doesn’t have a match guard and therefore matches any Some variant.
There is no way to express the if x % 2 == 0 condition within a pattern, so the match guard gives us the ability to express this logic. The downside of this additional expressiveness is that the compiler doesn’t try to check for exhaustiveness when match guard expressions are involved.
When discussing Listing 19-11, we mentioned that we could use match guards to solve our pattern-shadowing problem. Recall that we created a new variable inside the pattern in the match expression instead of using the variable outside the match. That new variable meant we couldn’t test against the value of the outer variable. Listing 19-27 shows how we can use a match guard to fix this problem.
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
Some(n) if n == y => println!("Matched, n = {n}"),
_ => println!("Default case, x = {x:?}"),
}
println!("at the end: x = {x:?}, y = {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.
The match guard if n == y is not a pattern and therefore doesn’t introduce new variables. This y is the outer y rather than a new y shadowing it, and we can look for a value that has the same value as the outer y by comparing n to y.
You can also use the or operator | in a match guard to specify multiple patterns; the match guard condition will apply to all the patterns. Listing 19-28 shows the precedence when combining a pattern that uses | with a match guard. The important part of this example is that the if y match guard applies to 4, 5, and 6, even though it might look like if y only applies to 6.
fn main() {
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}
}
The match condition states that the arm only matches if the value of x is equal to 4, 5, or 6 and if y is true. When this code runs, the pattern of the first arm matches because x is 4, but the match guard if y is false, so the first arm is not chosen. The code moves on to the second arm, which does match, and this program prints no. The reason is that the if condition applies to the whole pattern 4 | 5 | 6, not just to the last value 6. In other words, the precedence of a match guard in relation to a pattern behaves like this:
(4 | 5 | 6) if y => ...
rather than this:
4 | 5 | (6 if y) => ...
After running the code, the precedence behavior is evident: If the match guard were applied only to the final value in the list of values specified using the | operator, the arm would have matched, and the program would have printed yes.
Using @ Bindings
The at operator @ lets us create a variable that holds a value at the same time we’re testing that value for a pattern match. In Listing 19-29, we want to test that a Message::Hello id field is within the range 3..=7. We also want to bind the value to the variable id so that we can use it in the code associated with the arm.
fn main() {
enum Message {
Hello { id: i32 },
}
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello { id: id @ 3..=7 } => {
println!("Found an id in range: {id}")
}
Message::Hello { id: 10..=12 } => {
println!("Found an id in another range")
}
Message::Hello { id } => println!("Found some other id: {id}"),
}
}
@ to bind to a value in a pattern while also testing itThis example will print Found an id in range: 5. By specifying id @ before the range 3..=7, we’re capturing whatever value matched the range in a variable named id while also testing that the value matched the range pattern.
In the second arm, where we only have a range specified in the pattern, the code associated with the arm doesn’t have a variable that contains the actual value of the id field. The id field’s value could have been 10, 11, or 12, but the code that goes with that pattern doesn’t know which it is. The pattern code isn’t able to use the value from the id field because we haven’t saved the id value in a variable.
In the last arm, where we’ve specified a variable without a range, we do have the value available to use in the arm’s code in a variable named id. The reason is that we’ve used the struct field shorthand syntax. But we haven’t applied any test to the value in the id field in this arm, as we did with the first two arms: Any value would match this pattern.
Using @ lets us test a value and save it in a variable within one pattern.
Résumé
Rust’s patterns are very useful in distinguishing between different kinds of data. When used in match expressions, Rust ensures that your patterns cover every possible value, or your program won’t compile. Patterns in let statements and function parameters make those constructs more useful, enabling the destructuring of values into smaller parts and assigning those parts to variables. We can create simple or complex patterns to suit our needs.
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.