Match Ergonomics Changes
Match ergonomics is the feature in Rust (originally introduced in Rust 2018) that allows matching a reference &Option<T> or &mut Option<T> directly against a value pattern Some(x) without writing &Some(ref x). When the pattern matcher steps past a reference & or &mut, it switches its internal default binding mode from by-value (move) to by-reference (ref or ref mut).
In the Rust 2024 edition, match ergonomics were simplified and made more consistent by disallowing explicit mut, ref, or ref mut modifiers inside a pattern after the default binding mode has already switched to by-reference, and by preventing & patterns from simultaneously dereferencing a reference and resetting the binding mode to by-value (mut reset bug).
Matching a Reference Scrutinee `let opt: &Option<i32> = &Some(42);`
Rust 2021 (Confusing Mixing of Implicit Reference Mode + Explicit `mut`/`ref`):
`if let Some(mut x) = opt`
--> Stepping past `&Option` switched mode to `ref`, and then `mut x`
silently reset the binding mode back to BY-VALUE (`x: i32` copied)!
Rust 2024 (Consistent & Explicit Pattern Rules):
- To bind by shared reference (`x: &i32`): `if let Some(x) = opt`
- To copy out by value into `mut x: i32`: `if let &Some(mut x) = opt` (or `let mut x = *x;`)
Why Rust 2021's mut Binding Reset Was Error-Prone
In Rust 2021, when matching opt: &Option<i32>:
- Writing
if let Some(x) = optboundx: &i32(by reference, via match ergonomics). - However, merely adding
mut—if let Some(mut x) = opt—silently reset the binding mode from by-reference back to by-value, bindingx: i32(a mutable local copy) instead of&i32! Worse yet, if the inner type was notCopy(such as&Option<String>), writingif let Some(mut s) = optsuddenly failed withcannot move out of opt!
In Rust 2024, once match ergonomics has inherited a reference (& or &mut from the scrutinee), writing mut, ref, or ref mut on a binding inside that pattern is a compile error:
// ❌ does not compile
fn main() {
let opt: &Option<i32> = &Some(10);
// In Rust 2024, `mut x` is rejected when match ergonomics is already in `ref` mode:
if let Some(mut x) = opt {
x += 1;
println!("{x}");
}
}
error: `mut` on a binding may not be affected by default binding mode
--> src/main.rs:5:17
|
5 | if let Some(mut x) = opt {
| ^^^^^
|
= note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/match-ergonomics.html>
help: match the reference with `&` or dereference the scrutinee `*opt`
|
5 | if let &Some(mut x) = opt {
| +
Clean, Unambiguous Patterns in Rust 2024
| Scrutinee Type | Desired Binding for x |
Idiomatic Rust 2024 Pattern |
|---|---|---|
opt: &Option<T> |
x: &T (shared borrow) |
if let Some(x) = opt |
opt: &mut Option<T> |
x: &mut T (mutable borrow) |
if let Some(x) = opt (no ref mut needed!) |
opt: &Option<i32> (Copy) |
mut x: i32 (mutable local copy) |
if let Some(mut x) = *opt (or if let &Some(mut x) = opt) |
opt: Option<T> (by value) |
x: &T (borrow without moving opt) |
if let Some(x) = &opt (prefer &opt on the right over Some(ref x)!) |
fn main() {
// 1. Borrowing `Option<String>` via `&opt` on the scrutinee (cleanest style!):
let owned_opt = Some(String::from("rust-2024"));
if let Some(text_ref) = &owned_opt {
// `text_ref` is `&String` automatically
println!("Borrowed via &owned_opt: {text_ref}");
}
// 2. Mutating in-place via `&mut owned_opt`:
let mut mutable_opt = Some(vec![1, 2, 3]);
if let Some(vec_mut) = &mut mutable_opt {
// `vec_mut` is `&mut Vec<i32>` automatically
vec_mut.push(4);
}
assert_eq!(mutable_opt, Some(vec![1, 2, 3, 4]));
// 3. Copying a `Copy` value out of a reference by dereferencing `*ref_opt`:
let ref_opt: &Option<i32> = &Some(41);
if let Some(mut local_copy) = *ref_opt {
local_copy += 1;
assert_eq!(local_copy, 42);
println!("Mutated local copy from *ref_opt: {local_copy}");
}
}
Borrowed via &owned_opt: rust-2024
Mutated local copy from *ref_opt: 42