Pattern Syntax in Rust
Patterns are a fundamental feature in Rust, appearing in match expressions, if let statements, while let conditional loops, and variable declarations. This chapter explores all valid syntax options available inside patterns, ranging from literal matching and destructuring to advanced techniques like match guards and the @ operator.
1. Matching Against Literals
The most fundamental pattern matching involves comparing an incoming value directly against concrete literals (numbers, strings, or booleans).
fn main() {
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("default"),
}
}
- Concrete Action: Useful when your code needs to execute specific branches upon encountering exact, hardcoded values.
- Exhaustiveness: Rust requires
matchexpressions to be exhaustive, meaning every possible value must be accounted for (often handled via a catch-all_arm).
2. Named Variables in Patterns
Patterns can bind matched data to named variables for use within the match arm's scope. However, introduce caution regarding scope shadowing.
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("got 50"),
Some(y) => println!("matched y = {y}"), // shadows outer y
_ => println!("default case x = {:?}", x),
}
println!("outer y remains: {y}"); // prints 10
}
Note: A
matchblock creates a new inner scope. Variables defined or bound within the pattern (such asSome(y)) shadow any variables with the same name declared outside the match block. The innerycaptures whatever value is inside theSomevariant.
3. Matching Multiple Patterns
You can match against multiple alternatives within a single match arm using the | (OR) operator.
fn main() {
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 | 4 | 5 => println!("three, four, or five"),
_ => println!("something else"),
}
}
- Flexibility: The patterns combined with
|can be simple literals or complex structures, as long as both sides bind the same variables if binding is used.
4. Matching Ranges of Values
Rust allows matching inclusive ranges of numeric values or characters using the range operator (..=).
fn main() {
let x = 5;
match x {
1..=5 => println!("between 1 and 5 (inclusive)"),
_ => println!("out of range"),
}
let c = 'c';
match c {
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"),
}
}
- Type Restrictions: The range operator (
..=) only works on primitive numeric types (i32,u32, etc.) and characters (char) because Rust needs to know if the range is empty at compile time.
5. Destructuring Structs
You can break apart (destructure) custom structs to access their inner fields directly within patterns.
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
// Destructure into custom variable names
match p {
Point { x: a, y: b } => println!("a: {a}, b: {b}"),
}
// Shorthand syntax when variable names match field names
match p {
Point { x, y } => println!("x: {x}, y: {y}"),
}
// Matching against specific structural values (e.g., axes)
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})"),
}
}
6. Destructuring Enums and Nested Structures
Enums containing data variants (tuple, struct, or unit-like variants) can be seamlessly destructured inside match arms.
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::Quit => println!("Quit variant"),
Message::Move { x, y } => println!("Move to x: {x}, y: {y}"),
Message::Write(text) => println!("Text message: {text}"),
Message::ChangeColor(Color::Rgb(r, g, b)) => {
println!("RGB color: red {r}, green {g}, blue {b}");
}
Message::ChangeColor(Color::Hsv(h, s, v)) => {
println!("HSV color: hue {h}, saturation {s}, value {v}");
}
}
}
- Deep Nesting: You can match through nested enums and structs in a single concise expression without writing nested
ifstatements.
7. Ignoring Values in Patterns
Rust provides several mechanisms for ignoring whole values or parts of values when you do not need them.
A. Using the Underscore (_) as a Wildcard
fn foo(_: i32, y: i32) {
println!("Only using y: {y}");
}
fn main() {
foo(10, 20); // First argument is completely ignored
}
B. Ignoring Parts of a Value in Tuples or Structs
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => {
println!("Matched: {first}, {third}, {fifth}");
}
}
}
C. Underscore Prefix vs. Pure Underscore
Prefixing a variable with an underscore (_s) suppresses compiler warnings about unused variables, but it still moves or borrows the value. A pure underscore (_) does not bind the value at all.
fn main() {
let s = Some(String::from("Hello"));
// Using _s still binds and moves the String out of s!
if let Some(_s) = s {
println!("Found a string!");
}
// println!("{:?}", s); // Error: s was moved into _s!
let s2 = Some(String::from("Hello"));
// Using pure _ ignores the value without moving it
if let Some(_) = s2 {
println!("Ignored inner value safely");
}
println!("s2 is still accessible: {:?}", s2); // Works fine!
}
D. Ignoring Remaining Parts with ..
The .. syntax ignores any remaining parts of a value that you haven't explicitly listed.
struct Point {
x: i32,
y: i32,
z: i32,
}
fn main() {
let origin = Point { x: 0, y: 7, z: 9 };
match origin {
Point { x, .. } => println!("x is {x}, ignored y and z"),
}
let numbers = (element1, _, _, _, last_element); // Conceptual expansion
let vals = (1, 2, 3, 4, 5);
match vals {
(first, .., last) => println!("First: {first}, Last: {last}"),
}
}
Note: The
..range syntax must be unambiguous. You cannot place..in two places within the same slice or tuple pattern, as the compiler wouldn't know how many items to skip on each side.
8. Match Guards
A match guard is an additional if condition specified after the pattern in a match arm that provides extra filtering.
fn main() {
let num = Some(4);
match num {
Some(x) if x < 5 => println!("Less than five: {x}"),
Some(x) => println!("Greater than or equal to five: {x}"),
None => (),
}
}
Solving Shadowing with Match Guards
Match guards let you reference outer variables without triggering pattern shadowing bugs:
fn main() {
let x = Some(10);
let y = 10;
match x {
Some(n) if n == y => println!("Matched outer y! Value is {n}"),
_ => println!("Default case"),
}
}
Combining Match Guards with Multiple Patterns (|)
When using | with match guards, the if condition applies to all patterns in that arm, not just the last one.
fn main() {
let x = 4;
let y = true;
match x {
4 | 5 | 6 if y => println!("Matched 4, 5, or 6 and y is true"),
_ => println!("Did not match"),
}
}
9. The @ Operator (Bindings)
The at operator (@) lets you test whether a value matches a pattern while simultaneously creating a variable to hold that value.
enum Message {
Hello { id: i32 },
}
fn main() {
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 higher range, but didn't bind it");
}
Message::Hello { id } => {
println!("Found some other id: {id}");
}
}
}
- Dual Purpose: Allows testing complex conditions (like ranges) while still capturing the exact data value into a local variable for further processing.
Summary and Key Takeaways
- Literal Matching: Match directly against exact values (
1,2,'a'). - Named Variables: Bind inner data to local variables; beware of scope shadowing inside match blocks.
- Multiple Patterns: Combine alternative patterns using the
|operator (e.g.,1 | 2 | 3). - Ranges: Match inclusive numeric or character ranges using
..=(e.g.,1..=5). - Destructuring: Unpack structs, enums, tuples, and nested variants directly inside patterns.
- Ignoring Values: Use
_to ignore values completely, or prefix variable names with_to suppress compiler warnings while still taking ownership. - Remaining Syntax (
..): Skip arbitrary trailing or leading tuple/struct fields unambiguously. - Match Guards: Add extra filtering logic to match arms using
if <condition>. - The
@Operator: Test patterns (like ranges) and bind the matched value to a variable simultaneously.