Rust 67 🦀 Advanced Pattern Matching: Guards and @ Bindings
Pattern matching in Rust is more than a switch statement. Two features elevate it from simple value comparison to a concise expression of complex conditions: match guards and @ bindings. A guard is an if condition attached to a pattern that must also be true for the arm to match . An @ binding lets you test a value against a pattern while simultaneously binding the matched value to a name for use in the arm body . Together, they let you express rules that would otherwise require nested if statements or redundant reconstruction of values.
The reason these features matter is that patterns alone are structural. They can match a range, a variant, or a shape, but they cannot call a method, check a computed property, or express a condition that depends on something outside the pattern. A guard fills that gap. And a pattern alone can test a value without preserving it—if you match 3..=7, you know the value is in that range, but you do not have the value itself. An @ binding fills that gap . Neither feature is exotic; both appear in ordinary Rust code once you know to reach for them.
Key point: A match guard is an if condition after a pattern that must evaluate to true for the arm to match; an @ binding (name @ pattern) tests a value against a pattern and binds the matched value to name in the same step.
Why guards and @ bindings exist
The expressiveness gap. A pattern can say “this is a Some containing a number from 1 to 12.” It cannot say “this is a Some containing a number that is less than the value of split,” where split is a variable in scope . That is a guard’s job. The guard runs after the pattern matches structurally, and if it fails, the match continues to the next arm .
The binding gap. A pattern like 3..=7 matches a range but discards the matched value. If the arm body needs to print the actual number, you would have to match a variable (n) and then re-check the range inside the arm. That re-check does not contribute to exhaustiveness checking, and it duplicates the range . The @ binding solves both problems: n @ 3..=7 tests the range and binds the value in one step.
The exhaustiveness problem with guards. A guard is not part of the pattern for exhaustiveness purposes. The compiler cannot prove that a guarded arm covers all cases, because the guard might fail. This is why the n @ 3..=7 form is often preferable to n if (3..=7).contains(&n): the range pattern contributes to exhaustiveness, while the guard does not . The compiler will still require a fallback arm.
The | precedence problem. When a guard is combined with an or-pattern, the guard applies to the entire pattern, not just the last alternative. 4 | 5 | 6 if y means “if the value is 4, 5, or 6, and y is true” . This is a common source of confusion, and it is a reason to use parentheses if the intent differs.
a. Match guards: conditions on patterns
A match guard is written as pattern if condition => expression. The condition is a bool expression that can use any variables bound by the pattern .
fn main() {
let num = Some(4);
let split = 5;
match num {
Some(x) if x < split => println!("less than five: {}", x),
Some(x) => println!("{}", x),
None => (),
}
}
The first arm matches structurally as Some(x), then the guard checks x < split. If the guard fails, the match proceeds to the second arm, which matches any Some. The guard can reference variables bound in the pattern (x) and variables from the surrounding scope (split).
Guards are only available in match expressions, not in if let or while let. This is a deliberate limitation: if let already has an if for its condition, and adding a guard syntax would be redundant. If you need guard-like behavior in an if let, you can use the if clause directly: if let Some(x) = num && x < 5.
The guard is evaluated after the pattern matches, but before the value is moved into the arm’s bindings. The Rust Reference notes that a shared reference is taken to the matched part while evaluating the guard, and the value is moved only if the guard succeeds . This means you can use shared borrows in a guard without moving out of the scrutinee.
A subtle interaction: if an or-pattern has a guard, and the guard has side effects, the guard may run multiple times—once for each alternative that matches structurally . This is rarely a problem in practice, but it is worth knowing if you are debugging.
b. @ bindings: testing and binding at once
The @ operator has the form name @ pattern. It tests the value against the pattern and, if the pattern matches, binds the entire matched value to name.
fn main() {
let age = 15;
match age {
0 => println!("no birthday yet"),
n @ 1..=12 => println!("child of age {}", n),
n @ 13..=19 => println!("teen of age {}", n),
n => println!("adult of age {}", n),
}
}
The n @ 1..=12 arm matches any value in that range and binds it to n. Without the @, the arm 1..=12 would match but the body would have no access to the actual value . The final n arm is a catch-all binding, and it is required for exhaustiveness because the range patterns do not cover all u32 values.
The binding captures the value matched by the subpattern, not the textual pattern. In id @ 3..=7, id is the actual integer, not the string "3..=7". This is what makes it useful: the arm body can print, log, or pass the value on.
@ also works with enum variants and nested patterns. e @ Event::Click(..) binds the entire Event value while also matching the Click variant . Nested bindings are possible: outer @ OuterStruct { inner @ InnerStruct { value } } binds at multiple levels . The binding mode follows normal pattern rules—if the value is owned and not Copy, the @ binding may move it, so match on a reference if you need a borrowed binding .
A common use case is error reporting. When a value is out of range, you want to include the value in the error message. a @ 130..=u32::MAX => Err(format!("age {} is unrealistic", a)) binds the invalid value and uses it in the error . Without @, you would match the range and then have to re-extract the value, which is impossible without another match or a guard.
c. Combining guards and @ bindings
Guards and @ bindings compose. A guard can reference the name bound by @, and an @ binding can be used inside a guarded arm.
fn classify(value: Option<i32>) -> String {
match value {
Some(n @ 1..=5) if n % 2 == 0 => format!("small even: {}", n),
Some(n @ 1..=5) => format!("small odd: {}", n),
Some(n) => format!("large: {}", n),
None => "none".to_string(),
}
}
The first arm matches a Some containing a value in 1..=5, binds it to n, and then checks if it is even. The guard uses the bound name. If the value is 4, the first arm matches. If it is 3, the guard fails and the second arm matches. This pattern—range binding plus a guard for a finer condition—is a common way to avoid nested if statements.
The exhaustiveness rule still applies. The compiler sees the range 1..=5 as contributing to exhaustiveness, but the guard on the first arm does not. The second arm, without a guard, covers the same range. The third arm covers everything else. If the second arm were removed, the compiler would complain that the 1..=5 range is not exhaustively handled, even though the first arm’s guard might cover the even numbers .
Complete Example Session
The following session builds a small classifier that uses guards and @ bindings to route messages by ID range and parity.
// ============================================
// PART 1: ENUM DEFINITION
// ============================================
enum Message {
Hello { id: u32 },
Goodbye,
}
// ============================================
// PART 2: GUARD AND @ BINDING IN MATCH
// ============================================
fn route(msg: Message) -> String {
match msg {
// Bind the id and check if it is even
Message::Hello { id: n @ 3..=7 } if n % 2 == 0 => {
format!("priority even: {}", n)
}
// Bind the id for the odd case
Message::Hello { id: n @ 3..=7 } => {
format!("priority odd: {}", n)
}
// No binding needed for the reserved range
Message::Hello { id: 10..=12 } => {
String::from("reserved")
}
// Bind any other id
Message::Hello { id: n } => {
format!("normal: {}", n)
}
Message::Goodbye => String::from("bye"),
}
}
// ============================================
// PART 3: TESTING THE ROUTES
// ============================================
fn main() {
assert_eq!(route(Message::Hello { id: 4 }), "priority even: 4");
assert_eq!(route(Message::Hello { id: 5 }), "priority odd: 5");
assert_eq!(route(Message::Hello { id: 11 }), "reserved");
assert_eq!(route(Message::Hello { id: 20 }), "normal: 20");
assert_eq!(route(Message::Goodbye), "bye");
println!("all tests passed");
}
The first two arms use n @ 3..=7 to bind the ID and a guard to distinguish even from odd. The third arm matches a range without binding, because the arm body does not need the value. The fourth arm binds any remaining ID. The Goodbye arm has no data to bind. The assertions verify that each route behaves as intended.
Quick Reference
Guard Syntax
| Form | Example | Meaning |
|---|---|---|
| Pattern + condition | Some(x) if x < 5 | Match Some and check condition |
| Guard with or-pattern | 4 | 5 | 6 if y | Guard applies to all alternatives |
| Guard referencing binding | Some(n) if n % 2 == 0 | Use bound name in guard |
@ Binding Syntax
| Form | Example | Meaning |
|---|---|---|
| Bind range | n @ 1..=12 | Test range and bind value |
| Bind variant | e @ Event::Click(..) | Bind whole enum value |
| Bind nested | outer @ Some(inner @ Point { .. }) | Bind at multiple levels |
| Bind literal | x @ 42 | Test exact value and bind |
Guards vs @ Bindings
| Feature | Guards | @ Bindings |
|---|---|---|
| Purpose | Add runtime condition | Test pattern and bind value |
| Exhaustiveness | Does not contribute | Contributes via subpattern |
Available in if let | No | Yes |
| Can call methods | Yes | No (pattern only) |
Common Patterns
| Goal | Pattern |
|---|---|
| Range + use value | n @ 1..=12 |
| Range + extra condition | n @ 1..=12 if n % 2 == 0 |
| Variant + use whole value | e @ Event::Click(..) |
| Exact value + use value | x @ 42 |
Best Practices
✅ Do This:
n @ 1..=12 => println!("child of age {}", n) // ✅ bind and test range
Some(x) if x < split => process(x) // ✅ guard on bound value
e @ Event::Click(..) => log(e) // ✅ bind whole variant
❌ Don’t Do This:
n if (1..=12).contains(&n) => println!("{}", n) // ❌ no exhaustiveness
Some(x) => if x < split { process(x) } // ❌ nested if, not guard
1..=12 => println!("child") // ❌ value not available
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Guard not contributing to exhaustiveness | Guards are runtime conditions | Use a range pattern plus a fallback |
@ binding moves value | Binding mode follows pattern rules | Match on a reference or use ref |
| Guard applies to only last or-alternative | Misreading precedence | Guard applies to entire | pattern |
@ on an expression | Right side must be a pattern | Move logic to a guard |
Guard not available in if let | Language limitation | Use if let with a separate if |
Real-World Examples
1. Age classification with @
n @ 1..=12 => println!("child: {}", n)
2. Even/odd with guard
Some(n) if n % 2 == 0 => println!("even: {}", n)
3. Bind whole event
e @ Event::Click(..) => log(e)
4. Range + guard
n @ 1..=5 if n > 2 => println!("mid-range: {}", n)
5. Nested @ bindings
outer @ Some(inner @ Point { x, y }) => println!("{:?} {:?}", outer, inner)
6. Error with bound value
a @ 130..=u32::MAX => Err(format!("invalid age: {}", a))
7. Bind slice rest
whole @ [first, ..] => println!("first: {}, whole: {:?}", first, whole)
8. Guard on enum variant
Some(x) if x > 10 => process(x)
9. Bind exact value
x @ 42 => println!("the answer: {}", x)
10. Guard with or-pattern
4 | 5 | 6 if y => println!("yes")
Visual
Guard Flow
┌──────────────────────────────────────────────┐
│ match value │
│ │ │
│ ▼ │
│ Pattern matches structurally? │
│ ├─ NO → next arm │
│ └─ YES → evaluate guard │
│ │ │
│ ┌────┴────┐ │
│ │ │ │
│ true false │
│ │ │ │
│ ▼ ▼ │
│ run arm next arm │
└──────────────────────────────────────────────┘
@ Binding
┌──────────────────────────────────────────────┐
│ n @ 1..=12 │
│ │ │
│ ▼ │
│ Test: is value in 1..=12? │
│ ├─ NO → arm does not match │
│ └─ YES → bind value to n │
│ │ │
│ ▼ │
│ arm body can use n │
└──────────────────────────────────────────────┘
Exhaustiveness with Guards
┌──────────────────────────────────────────────┐
│ n @ 1..=5 if n % 2 == 0 => ... │
│ n @ 1..=5 => ... │
│ n => ... │
│ │
│ Guard does NOT contribute to exhaustiveness.│
│ The second arm (no guard) covers 1..=5. │
│ The third arm covers everything else. │
│ Without the second arm, compiler error. │
└──────────────────────────────────────────────┘
@ vs Guard
┌──────────────────────────────────────────────┐
│ @ BINDING (structural) │
│ n @ 1..=12 │
│ Tests range, binds value │
│ Contributes to exhaustiveness │
│ │
│ GUARD (runtime) │
│ Some(x) if x % 2 == 0 │
│ Tests condition, does not bind │
│ Does not contribute to exhaustiveness │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Match guard | pattern if condition |
| Guard availability | match only, not if let |
@ binding | name @ pattern |
| Range binding | n @ 1..=12 |
Guard with | | Applies to entire pattern |
| Exhaustiveness | @ contributes, guard does not |
Nested @ | outer @ Some(inner @ ...) |
| Binding mode | Follows normal pattern rules |
Key takeaways:
- Guards add runtime conditions to patterns. They are evaluated after the pattern matches, and if they fail, the match continues to the next arm .
- Guards do not contribute to exhaustiveness. The compiler cannot prove a guarded arm covers all cases, so a fallback is still required .
@binds and tests in one step.n @ 1..=12checks the range and makes the value available in the arm body .- Guards can use
@-bound names.n @ 1..=5 if n % 2 == 0is a valid and common combination . - Guards apply to the whole or-pattern.
4 | 5 | 6 if ymeans all three alternatives are guarded, not just6. @bindings can be nested. You can bind at multiple levels of a struct or enum pattern simultaneously .- Guards are not available in
if let. Use theifclause directly for conditional matching inif let.
Remember: Match guards and @ bindings are the two features that make Rust’s pattern matching more than a switch. A guard lets you express a condition that the pattern alone cannot capture, at the cost of exhaustiveness checking. An @ binding lets you keep the value you matched, at the cost of a slightly longer pattern. Use @ when the arm body needs the value; use a guard when the arm body needs a condition that the pattern cannot express. And when you combine them, remember that the guard applies to the entire pattern—not just the last alternative—and that the compiler will still demand a fallback arm.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!