12.3 Binding Variables in Patterns
Now, let’s get our hands dirty with one of the most powerful features of pattern matching: binding. This is where we move from simply checking if a pattern matches to actually extracting the juicy data inside the matched value and giving it a name. It’s the difference between a bouncer just nodding you in and him also handing you a map of the party’s best spots.
Consider our old friend, the Option<T>. Without binding, you can check if it’s Some or None, but you’re left awkwardly staring at it, unable to get to the T inside the Some. Binding solves this with elegant, surgical precision.
let some_number = Some(42);
match some_number {
Some(value) => println!("The answer is {value}"), // `value` is bound here!
None => println!("There's nothing to see here"),
}
See that value inside the Some(pattern)? That’s a new variable, bound to the inner contents of the Some variant. For the Some(42) case, value is now the i32 with the value 42. It’s not magic; it’s just brilliantly designed variable destructuring.
The At Symbol (@) for Tests and Binds
Sometimes, you want to have your cake and eat it too. You want to match on a more complex pattern and bind the entire value to a variable. This is where @ comes in, and it’s a lifesaver for avoiding needless repetition.
Imagine you’re matching on a Some but you also care about the value inside meeting a certain condition. You could do this, but it’s clunky and repeats the variant name:
let some_number = Some(42);
match some_number {
Some(value) => {
if value > 50 {
println!("It's a big number: {value}");
} else {
println!("It's a small number: {value}");
}
}
None => (),
}
The @ binding lets you be far more succinct and expressive. It says “match this whole pattern, and if it fits, also bind the entire value to this variable.”
let some_number = Some(42);
match some_number {
captured @ Some(value) if value > 50 => {
println!("It's a big number: {value}. The whole thing was {captured:?}");
}
captured @ Some(value) => {
println!("It's a small number: {value}. The whole thing was {captured:?}");
}
captured => {
println!("It's something else: {captured:?}");
}
}
Here, captured is bound to the entire Option<i32> (e.g., Some(42)), while value is simultaneously bound to the inner i32 (e.g., 42). This is incredibly powerful for logging, for passing the original value elsewhere, or for conditions that need to reference both the whole and the part.
Ignoring Parts You Don’t Care About
A key tenet of Rust is that you should only handle what you explicitly mean to handle. Patterns are fabulous for this. Don’t want to bind a value? Use an underscore _. It’s the pattern matcher’s way of saying “I acknowledge something might be here, but I solemnly swear I will not use it.”
This is crucial for enums with variants that contain data you might not need for a specific case.
enum WebEvent {
PageLoad,
KeyPress(char, bool), // key, caps_lock_on
Paste(String),
// ... other variants
}
let event = WebEvent::KeyPress('q', true);
match event {
WebEvent::KeyPress(key, caps_on) => {
println!("You pressed {key}. Caps lock was {caps_on}.");
},
WebEvent::Paste(contents) => {
println!("You pasted {} glorious characters.", contents.len());
},
// We don't need any data from PageLoad, so we ignore it.
WebEvent::PageLoad => println!("Page loaded."),
// And we use `_` to ignore ANY other variant, present or future.
_ => (),
}
But what if the KeyPress variant had three fields and you only cared about the first one? You can use .. to ignore multiple fields at once. This is much clearer than binding them to _ over and over.
// A more complex event
enum DetailedEvent {
Click { x: i32, y: i32, button: u8 },
// ... others
}
let event = DetailedEvent::Click { x: 100, y: 200, button: 1 };
match event {
// We only care about the x coordinate here, so we ignore y and button.
DetailedEvent::Click { x, .. } => println!("Clicked at x = {x}"),
_ => (),
}
A Common Pitfall: Shadowing and Move Semantics
Here’s where your brilliance can briefly stumble. When you bind a variable in a pattern, you’re creating a new variable. If you reuse a name from an outer scope, you shadow it. This is usually fine, but it can be confusing.
let favorite_color = "blue";
let some_option = Some("red");
match some_option {
Some(favorite_color) => { // This creates a new `favorite_color` binding!
println!("Inside this arm, your favorite color is {favorite_color}"); // prints "red"
}
None => (),
}
println!("But out here, it's still {favorite_color}"); // prints "blue"
More critically, remember ownership. Binding a variable in a pattern will move or borrow the value, just like any other assignment. If you match on a struct that doesn’t implement Copy, you’ll move its fields right out of it!
struct FancyString(String);
let thing = FancyString("hello".to_string());
match thing {
// This moves the inner String out of `thing` and binds it to `s`
FancyString(s) => println!("Got the string: {s}"),
}
// println!("{:?}", thing.0); // This would fail! `thing` is partially moved.
The fix? Match on a reference (&thing) and then pattern match to borrow the inner data instead.
match &thing {
// Now we're borrowing the inner String, so `thing` remains intact.
FancyString(s) => println!("Got the string: {s}"), // s is now a &String
}
This is the kind of detail that separates okay Rust code from great Rust code. You’re not just checking shapes; you’re consciously controlling the flow of data ownership with every pattern you write.