25 KiB
Pattern Syntax
Here we gather all the syntax valid in patterns and discuss why and when you might want to use each one.
Matching Literals
As you saw previously in Ch6, you can match patterns against literals directly.
Here is an example of this
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
The 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 have used them many times.
However, there is a complication when you use named variables in match
, if let
, or while let
expressions.
Because each kinds of expression starts a new scope, variables declared as part of a pattern inside the expression will shadow those with the same name outside, as is the case with all variables.
Here we declare a variable named x
with the value Some(5)
and a variable y
with the value 10
.
Next we 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.
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}");
Lets run through what happens when the match
expression runs.
The pattern in the first match arm doesn't match the defined value of x
, so the code continues.
The pattern in the second match arm introduces a new variable named y
that will match any value inside a Some
value.
Because we are 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 the 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
.
If x
has been a None
value instead of Some(5)
, the patterns in the first two arms wouldn't have matched. so the value would have matched to the underscore.
We didn't introduce the x
variable in the pattern of the underscore arm, so the x
in the expression is still the outer x
that hasn't been shadowed.
For this hypothetical case, the maatch
would pint Default case, x = None
.
When the match
expression is done, its scope ends, and so does the scope of the inner y
.
The last println!
produces at the end: 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 which shadows the exiting y
variable.
We would need to use a match guard conditional instead.
This will be covered later in the ch.
Multiple Patterns
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
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.
Here when a pattern matches any of the values within the given range, that arm will execute
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 that using the |
operator to express the same idea.
If we were to use |
we would need to do 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, because 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 car
values.
Rust can tell that 'c'
is within the first pattern's range and prints early ASCII letter
.
Destructuring to Break Apart Values
We can also use patterns to destructure structs, enums, and tuples to use different parts of these values.
We will walk through each value.
Destructuring Structs
This code shows a Point
struct with two fields, x
and y
, that we can break apart using a patter 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 creates the variables a
and b
that match the values of the x
and y
fields of the p
struct.
This shows that the names of the variables in the pattern don't have to match the field names of the struct.
However it is 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.
This code behaves in the same way as om the code before, 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);
}
This code creates the variables x
and y
that match the x
and y
fields of the p
variable.
The outcome is that the variables x
and y
contain the values form the p
struct.
We can also destructure with literal values as part of the struct pattern rather than creating variables for all the fields.
Doing this allows us to test some of the fields for particular values while creating variables to destructure the other fields.
Here 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 Neither
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})");
}
}
}
The first arm will match any point that lies on the x
axis by specifying that the y
field matches if its value matches the literal 0
.
The pattern still creates an x
variable that we can use in the code for this arm.
The second arm matches any point on the y
axis by specifying that the x
field matches if its value is 0
and creates a variable y
for the value of the y
field.
The third arm doesn't specify any literals, so it matches any other Point
and creates variables that we can use in the code for this arm.
Here the value p
matches the second arm by virtue of x
containing a 0, so this code will print On the x axis at 0
.
Destructuring Enums
We have destructured enums before, but haven't explicitly discussed that the pattern to destructure an enum corresponds to the way the data stored within the enum is defined.
For example, here we use the Message
enum from Ch6 and write a match
with patterns that will destructure each inner value.
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(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 the color to red {r}, green {g}, and blue {b}");
}
}
}
This code will print Change the color to red 0, green 160, and blue 255
.
Try changing the value of msg
to see the code form the other arms run.
For enum variants without any data, like Message::Quit
.
We can't destructure the value any further.
We can only match on the literal Message::Quit
value and no variables are in that pattern.
For struct-like enum variants, like 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 variable so we break apart the pieces to use in the code for this arm.
We did use the shorthand form as we did before.
For tuple-like enum variants, like Message::Write
that holds a tuple with one element and Message::ChangeColor
that holds a tuple with three elements.
The pattern is similar to the pattern we specify to match tuples.
The number of variables in the pattern must match the number of elements in the variant we are matching.
Destructuring Nested Structs and Enums
So we have seen examples that have all been matching structs or enums on level deep.
Matching can work on nested items too.
For example, we can refactor the code form before to support RGB and HSV colors in the ChangeColor
message.
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(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 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 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.
Destructuring Structs and Tuples
We can also mix, match and nest destructuring patterns in even more complex ways.
This example shows a complicated destructure where we nest structs and tuples inside a tuple and destructure all the primitive values out
let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
Here the code lets us break complex types into their component parts so we can use the values we are interested in separately.
Destructuring with patters is a convenient way to use pieces of values such as the value from each field in a struct, separately from each other.
Ignoring Values in a Pattern
Sometimes it is useful to ignore values in a pattern, such as in the last arm of a match
, to get a catchall that doesn't actually do anything but does account for all remaining possible values.
There are a few ways to ignore entire values or pats of values in a pattern:
- Using the
_
pattern - Using the
_
pattern within another pattern - Using a name that starts with an underscore
- Using
..
to ignore remaining parts of a value.
Ignoring an Entire Value with _
We have used the underscore as a wildcard pattern that will match any value but not bind to the value.
This is particularly useful as the last arm in a match
expression.
We can also use it in any pattern, including function parameters
fn foo(_: i32, y: i32) {
println!("This code only uses the y parameter: {y}");
}
fn main() {
foo(3, 4);
}
This will completely ignore the value 3
that is passed as the first argument.
You 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 it doesn't include the used parameter.
Ignoring a function parameter can be especially useful in cases when you are implementing a trait when you need a certain type signature but the function body in your implementation doesn't need one of the parameters.
Thus then you avoid getting a compiler warning about unused function parameters, as you would if you used a name instead.
Ignoring Parts of a Value with a Nested _
You can also use _
inside another pattern to ignore just part of a value.
Lets say when you 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.
Here 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 setting but can unset the setting and give it a value if it is currently unset.
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:?}");
This will print Can't overwrite an existing customized value
and then setting is Some(5)
.
In the first 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 this 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
are None
) expressed by the _
pattern in the second arm.
We want to allow new_setting_value
to become setting_value
.
We could also use underscores in multiple places within one pattern to ignore particular vlaues.
Here shows an example of ignoring the second and fourth values in a tuple of five items.
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {first}, {third}, {fifth}")
}
}
This will print Some numbers: 2, 8, 32
, and the values 4 and 16 will be ignored.
Ignoring 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 is useful to be able to create a variable you will not use yet.
Such as when you are prototyping or just starting a project.
In this situation, you can tell Rust not to warn you about the unused variable by starting by starting the name of the variable with an underscore.
Here we create two unused variables, but when we compile, we should only get warning about one of them.
fn main() {
let _x = 5;
let y = 10;
}
We get a warning about not using the variable y
, but we don't get a warning about not using _x
.
The only 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, this will provide us with an error.
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{s:?}");
We will receive an error because the s
value will still be moved into _s
, which prevents us from using s
again.
Using the underscore by itself doesn't ever bind to the value.
This code will compile without an errors because s
doesn't get moved into _
.
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.
Ignoring Remaining Parts of a Value 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 this code we have a Point
struct that holds a coordinate in three-dimensional space.
In this match
expression, we want to operate only on the x
coordinate and ignore the values in the y
and z
fields.
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}"),
}
Here we list the x
value and then just include the ..
pattern.
This is far quicker than having to list y: _
and z: _
, particularly when we are working with structs that have lots of fields in situations where only one or two fields are relevant.
The syntax ..
will expand to as many values as it needs to be.
This is an example of how to use ..
with a tuple.
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, .., last) => {
println!("Some numbers: {first}, {last}");
}
}
}
Output
Some numbers: 2, 32
The fist and last value are matched with first
and last
.
The ..
will match and ignore everything in the middle.
Using ..
must be unambiguous.
If it unclear which values are intended for matching and which should be ignored, Rust will give us an error.
Here 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}")
},
}
}
We get this compiler 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
It is impossible for Rust to determine how many values in the tuple to ignore before matching a value with second
and then how many further values to ignore after.
This code could mean we want to ignore 2
, bind second
to 4
and then ignore 8
and 16
and 32
.
Or it could mean we want to ignore 2
and 4
, bind second
to 8
, and then ignore 16
and 32
.
Or any other case.
The variable name second
doesn't mean anything special to Rust, we get a compiler error because using ..
in two places like this ambiguous.
Extra 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 complex ideas than a pattern alone allows.
They are only available in match
expressions, not in if let
or while let
expressions.
The condition can use variables created in the pattern.
Here shows a match
where the first arm has the pattern Some(x)
and also has a match guard of if x % 2 == 0
This will be true if the number is even.
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 will print The number 4 is even
.
When num
is compared to the pattern in the first arm, it matches.
This is because Some(4)
matches Some(x)
.
Next the match guard checks whether the remainder of dividing x
by 2 is equal to 0.
Because this is true the first arm is selected.
If num
had been Some(5)
instead, the match guard in the first arm would have been false.
This is because the remainder of 5 / 2 is 1, which is not equal to 0.
Rust would then go to the second arm, which doesn't have a match guard and therefore matches any Some
variant.
There is not may 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 is that the compiler doesn't try to check for exhaustiveness when match guard expressions are involved.
Before we mentioned that we could use match guards to solve our pattern shadowing problem.
Recall that after we created a new variable inside the pattern in the match
expression instead of using the variable outside the match
.
This new variable meant we couldn't test against the value of the outer variable.
Here shows how we can use 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}");
}
Output
Default case, x = Some(5)
at the end: x = Some(5), y = 10
The pattern in the second match arm doesn't introduce a new variable y
that would shadow the outer y
.
This means that we can use the outer y
in the match guard.
Instead of specifying the pattern as Some(y)
, which would have shadowed the outer y
, we specify Some(n)
.
This creates a new variable n
that doesn't shadow anything because there is no n
variable outside the 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.
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.
Here shows the precedence when combining a pattern 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
.
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}
This match condition states that the arm only matches if the if the value of x
is equal to 4
, 5
, or 6
and if y
is true
.
When this 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 then 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 only 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 this, the precedence behavior is evident.
If the match guard 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
.
@
Bindings
The at operator @
lets us create a variable that holds a value at the same time as we are testing that value for a pattern match.
Here we want to test that a Message::Hello
id
field is within the range 3..=7
.
We also want to bind that value to the variable id_variable
so we can use it in the code associated with the arm.
We could name this variable id
, the same as the field, but for this example we will use a different name.
enum Message {
Hello { id: i32 },
}
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello {
id: id_variable @ 3..=7,
} => println!("Found an id in range: {id_variable}"),
Message::Hello { id: 10..=12 } => {
println!("Found an id in another range")
}
Message::Hello { id } => println!("Found some other id: {id}"),
}
Output
Found an id in range: 5
By specifying id_vaariable @
before the range 3..=7
, we are capturing whatever value matched the range 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 values could have been 10, 11, or 12, but the code that goes with the pattern doesn't know which it is.
The pattern code isn't able to use the value form the id
field, this is due to us not saving the id
value in a variable.
In the last arm, where we have specified a variable without a range, we do have the value available to use in the arm's code in the variable named id
.
The reason is that we have 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.