18 - Patterns and Matching
18.1 - All the Places Patterns Can Be Used
We've already talked about using patterns in match
and if let
expressions, but actually patterns are everywhere in Rust. A destructuring assignment is actually an example of a pattern.
match
Arms
As seen in chapter 6, the arms of a match
use patterns:
match VALUE {
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
PATTERN => EXPRESSION,
}
The patterns in a match
need to be exhaustive - they need to cover every possibility. The _
pattern will match anything and not bind to a variable, so it will often be used as a catch-all at the end of a match
.
In this example, we extract a value from a Some
. Note that value we extract in this example will shadow the outer variable:
match i {
None => None,
Some(i) => Some(i + 1),
}
Conditional if let
Expressions
In chapter 6 we also so how to use an if let
expression. These use patterns just like a match
, but they don't have to be exhaustive (which can be an advantage or a disadvantage, depending on the situation) and we can mix patterns with different values. This example uses a number of different inputs to decide what color to use as a background color:
fn main() {
let favorite_color: Option<&str> = None;
let is_tuesday = false;
let age: Result<u8, _> = "34".parse();
if let Some(color) = favorite_color {
println!("Using your favorite color, {color}, as the background");
} else if is_tuesday {
println!("Tuesday is green day!");
} else if let Ok(age) = age {
if age > 30 {
println!("Using purple as the background color");
} else {
println!("Using orange as the background color");
}
} else {
println!("Using blue as the background color");
}
}
Note in this example we used let Ok(age) = age
to create a shadowed variable for age
, similar to what we did in our match
example above.
while let
Conditional Loops
We can create a while let
loop, which is very similar to the if let
syntax:
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);
// This prints 3, 2, then 1. When `pop`
// returns `None`, this loop will stop.
while let Some(top) = stack.pop() {
println!("{}", top);
}
for
Loops
In a for
loop, the bit immediately after the for
keyword is actually a pattern! We can use this to destructure values from an iterator:
let v = vec!['a', 'b', 'c'];
for (index, value) in v.iter().enumerate() {
println!("{} is at index {}", value, index);
}
let
statements
Simple let statements use patterns too:
let x = 5;
x
here is a pattern, albeit a very boring one. Using it here is similar to using x
as a pattern in a match
. The fact that this is a pattern is what makes it possible to do destructuring assignment in Rust:
let (x, y, z) = (1, 2, 3);
Function and Closure Parameters
Similar to let
, the parameters of a function are also patterns. We can use this to destructure a tuple or struct in a function declaration:
fn print_coordinates(&(x, y): &(i32, i32)) {
println!("Current location: ({}, {})", x, y);
}
fn main() {
let point = (3, 5);
print_coordinates(&point);
}
The matches!
Macro
Rust provides a handy macro that can be used to check if a value matches a specific pattern:
let foo = 'f';
assert!(matches!(foo, 'A'..='Z' | 'a'..='z'));
let bar = Some(4);
assert!(matches!(bar, Some(x) if x % 2 == 0));
18.2 - Refutability: Whether a Pattern Might Fail to Match
In this example:
match i {
None => None,
Some(1) => Some(2),
x => Some(x + 2),
}
Some(1)
and None
are examples of refutable patterns. Either of these patterns, taken alone, might or might not match i
. x
is an example of an irrefutable pattern. x
will always match, no matter what.
There are some places where we're only allowed to use irrefutable patterns. For example, consider the statement:
let Some(x) = value;
Here if value
is Some(1)
, then we expect x
to get the value 1. But if value
were None
, what would x
be here? This statement makes no sense, and will result in a compiler error, because an assignment needs an irrefutable pattern. (Although we could fix this with an if let
instead.)
There are also places where an irrefutable parameter is allowed, but is somewhat pointless, which will generate compiler warnings, such as this:
if let x = 5 {
println!("{}", x);
}
This is technically valid Rust code, but there aren't any good reasons to write something like this, so this is probably a mistake.
18.3 - Pattern Syntax
Matching Literals
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
Matching Named Variables
Named variables are irrefutable patterns that match any value:
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
y => println!("Matched, y = {y}"),
}
println!("at the end: x = {:?}, y = {y}", x);
Here y
will match any value. Note that y
does not match only 10
here.
Multiple Patterns
You can match more than one value with the |
"or operator", or with a range expression:
let x = 1;
match x {
1 | 2 => println!("one or two"),
3..=5 => println!("three, four, or five"),
_ => println!("anything"),
}
Destructuring to Break Apart Values
We can destructure a struct or tuple with a pattern:
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
// Can rename the values when we destructure:
let Point { x: a, y: b } = p;
assert_eq!(0, a);
assert_eq!(7, b);
// Or not:
let Point { x, y } = p;
assert_eq!(0, x);
assert_eq!(7, y);
}
We can also destructure with literal values as part of a pattern. The first two arms of this match
only match when y
is 0 or x
is 0, respectively:
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})");
}
}
}
We've seen examples already of destructuring enums:
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}",)
}
}
}
We can even destructure nested fields out of an enum:
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}")
}
_ => (),
}
}
And we can mix-and-match destructuring nested structs and tuples:
let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
Ignoring Values in a Pattern
We can ignore an entire value in a pattern with _
. We've seen this as a catch-all in a match
, but we can also use it to ignore a parameter in a function:
fn foo(_: i32, y: i32) {
println!("This code only uses the y parameter: {}", y);
}
fn main() {
foo(3, 4);
}
This can be useful when you need to implement a certain function signature in order to match the definition in a trait. We can also use _
to ignore parts of a value:
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);
Or part of a destructuring assignment:
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {first}, {third}, {fifth}")
}
}
Ignoring an Unused Variable by Starting Its Name with _
We can prefix an unused variable name with an _
to avoid a compiler warning:
fn main() {
let _x = 5;
}
There is one big difference between _
and _x
: _x
will still bind the variable, where _
will not. This can be important when we consider ownership:
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
// `s` was moved into `_s` above, so
// this won't compile!
println!("{:?}", s);
If we used if let Some(_)
here, this would have worked.
Ignoring Remaining Parts of a Value with ..
We can ignore part of a tuple or struct with ..
:
struct Point {
x: i32,
y: i32,
z: i32,
}
let origin = Point { x: 0, y: 0, z: 0 };
// Ignore the `y` and `z` members
match origin {
Point { x, .. } => println!("x is {}", x),
}
let numbers = (2, 4, 8, 16, 32);
// Ignore all but the first and last numbers
match numbers {
(first, .., last) => {
println!("Some numbers: {first}, {last}");
}
}
Extra Conditionals with Match Guards
A match guard is an additional if
condition attached to a pattern:
let num = Some(4);
match num {
Some(x) if x % 2 == 0 => println!("The number {} is even", x),
Some(x) => println!("The number {} is odd", x),
None => (),
}
A match guard applies to the entire pattern:
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}
Here if y
is true, the first arm will match 4 | 5 | 6
. If y
is false, the first arm will never match. It's (4 | 5 | 6) if y
, not 4 | 5 | (6 if y)
.
One downside to match guards is that they generally require the match to have a catch-all at the end. You and I might know that this match
is exhaustive:
match x {
Some(x) if y => println!("{x}"),
Some(x) if !y => println!("{x}"),
None => panic!("Silly compiler"),
}
But unfortunately the compiler isn't smart enough to figure this out.
One word of caution here (or any time you use a catch-all so you don't have to explicitly enumerate all cases):
let x = Some(8);
match x {
Non => panic!("Nothing to print"),
Some(x) if x <= 2 => println!("Little {x}"),
Some(x) if x > 2 => println!("Big {x}"),
}
You might expect this to print "Big 8", but it will actually print "Nothing to print" because we mistyped None
and accidentally created a variable named Non
. Fortunately it's difficult to do this without getting a compiler warning for our unused Non
variable.
@
Bindings
Sometimes we want to test a value as part of a pattern, and also assign that value to a variable. We can do this with the at operator:
enum Message {
Hello { id: i32 },
}
let msg = Message::Hello { id: 5 };
match msg {
// Match `id` and bind it to `id_variable`.
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),
}
Continue to chapter 19.