Patterns
We've actually used patterns a few times so far: they're used in let
statements, in function parameters, and in the match
expression. Patterns have
a lot more abilities than we have demonstrated so far, so we'll cover some of
the most commonly used ones in this section. Any of these abilities work in any
place where a pattern is used.
let
statements
A basic let
statement has this form:
let PATTERN = EXPRESSION;
We've seen statements like let x = 5;
with a variable name in the PATTERN
slot; a variable name is just a particularly humble form of pattern.
Let’s try a more complex pattern. Change our example program to this:
Filename: src/main.rs
fn main() {
let (x, y) = (5, 6);
println!("The value of x is: {}", x);
println!("The value of y is: {}", y);
}
And run it with cargo run
:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
Running `target/debug/patterns`
The value of x is: 5
The value of y is: 6
We’ve created two variables with one let
! Here’s our pattern:
(x, y)
And here’s the value:
(5, 6)
As you can see, the two line up visually, and so let
binds the variable x
to the value 5
and y
to 6
. We could have used two let
statements as
well:
fn main() {
let x = 5;
let y = 6;
}
In simple cases like this, two let
s may be clearer, but in others, creating
multiple variables at once is nice. As we become more proficient in Rust, we’ll
figure out which style is better, but it’s mostly a judgment call.
Type annotations
Most of the time, Rust uses type inference, meaning that it attempts to infer the types of your variables rather than you having to declare them explicitly even though Rust is a statically typed language. Occasionally, Rust won't have enough information to infer the type of your value, and you will need to add a type annotation in with the pattern.
Here’s what a let
statement with a type annotation looks like:
let x: i32 = 5;
We can add a colon, followed by the type name. Here’s the structure of a let
statement with a type annotation:
let PATTERN: TYPE = VALUE;
Note that the colon and the TYPE
go after the PATTERN
, not in the pattern
itself. As an example, here’s our more complex pattern with two variables:
let (x, y): (i32, i32) = (5, 6);
Just like we match up the VALUE
with the PATTERN
, we match up the TYPE
with the PATTERN
.
Literals & _
You can match against literals directly, and _
acts as an ‘any’ case:
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
This prints one
.
Multiple patterns
You can match multiple patterns with |
:
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
This prints one or two
.
ref and ref mut
Usually, when you match against a pattern, variables are bound to a value.
This means you'll end up moving the value into the match
:
let name = Some(String::from("Bors"));
match name {
Some(name) => println!("Found a name: {}", name),
None => (),
}
// name is moved here. This line will fail to compile:
println!("name is: {:?}", name);
If you'd prefer to bind name
to a reference, use the ref
keyword:
let name = Some(String::from("Bors"));
match name {
Some(ref name) => println!("Found a name: {}", name),
None => (),
}
// name is not moved here; the match only took a reference to its data rather
// than moving it. This will work:
println!("name is: {:?}", name);
And for a mutable reference, ref mut
:
let mut name = Some(String::from("Bors"));
match name {
Some(ref mut name) => *name = String::from("Another name"),
None => (),
}
// name is not moved here; the match only took a reference to its data rather
// than moving it
println!("name is: {:?}", name);
Destructuring
Patterns can be used to destructure structs and enums:
struct Point {
x: i32,
y: i32,
}
let origin = Point { x: 0, y: 0 };
let Point { x, y } = origin;
This brings x
and y
variables into scope, matching the x
and y
of
origin
. While it can be unusual in let
, this is the same principle of
patterns in match
:
struct Point {
x: i32,
y: i32,
}
let origin = Point { x: 0, y: 0 };
match origin {
Point { x, y } => { }, // variables x and y are created here
}
Shadowing
As with all variables, those declared by a pattern will shadow variables
outside of the match
construct:
let x = Some(5);
match x {
Some(x) => { }, // x is an i32 here, not an Option<i32>
None => (),
}
Ignoring values
We discussed using _
as a whole pattern to ignore it above, but you can
also use _
inside of another pattern to ignore just part of it:
let x = Some(5);
match x {
Some(_) => println!("got a Some and I don't care what's inside"),
None => (),
}
Or like this:
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => println!("Some numbers: {}, {}, {}", first, third, fifth),
}
If you want, you can use ..
to ignore all of the parts you haven't defined:
struct Point {
x: i32,
y: i32,
z: i32,
}
let origin = Point { x: 0, y: 0, z: 0 };
match origin {
Point { x, .. } => { }, // y and z are ignored
}
Ranges
You can match a range of values with ...
:
let x = 5;
match x {
1 ... 5 => println!("one through five"),
_ => println!("something else"),
}
Ranges are usually used with integers or char
s:
let x = 'c';
match x {
'a' ... 'j' => println!("early ASCII letter"),
'k' ... 'z' => println!("late ASCII letter"),
_ => println!("something else"),
}
Guards
You can introduce ‘match guards’ with if
:
let x = Some(5);
match x {
Some(x) if x < 5 => println!("less than five: {}", x),
Some(x) => println!("{}", x),
None => (),
}
If you’re using if with multiple patterns, the if applies to both sides:
let x = 4;
let y = false;
match x {
4 | 5 if y => println!("yes"),
_ => println!("no"),
}
This prints no
, because the if applies to the whole of 4 | 5
, and not to only
the 5
. In other words, the precedence of if behaves like this:
(4 | 5) if y => ...
not this:
4 | (5 if y) => ...
Bindings
You can bind values to names with @
: