Destructuring with let
In Rust, the left-hand side of a let statement is not limited to a single variable identifier—it is a pattern. Destructuring uses a pattern on the left-hand side of let to unpack a compound value (such as a tuple, array, or struct) into separate local variables in a single statement.
Anatomy of a Destructuring let Statement
When the pattern on the left mirrors the structure of the value on the right, Rust matches each piece of the value and binds it to the corresponding variable identifier:
Pattern Compound Value
+--------------+ +----------------+
let (id, score, tag) = (101, 98.5, 'A');
| | | | | |
| | +------------------+-----+----+---> tag = 'A' (char)
| +------------------------+-----+--------> score = 98.5 (f64)
+------------------------------+--------------> id = 101 (i32)
fn main() {
let record = (101, 98.5, 'A');
// Unpack all three elements into distinct bindings
let (id, score, grade) = record;
println!("ID: {id}, Score: {score}, Grade: {grade}");
}
ID: 101, Score: 98.5, Grade: A
Destructuring Tuples and Nested Structures
Tuple destructuring works for tuples of any arity and can unpack nested tuples of arbitrary depth in one step:
fn min_and_max(a: i32, b: i32) -> (i32, i32) {
if a < b { (a, b) } else { (b, a) }
}
fn main() {
let (smallest, largest) = min_and_max(42, 17);
println!("min = {smallest}, max = {largest}");
// Nested tuple destructuring
let nested = ((10, 20), ("width", 1920));
let ((x, y), (label, pixels)) = nested;
println!("Point ({x}, {y}), {label} = {pixels}");
}
min = 17, max = 42
Point (10, 20), width = 1920
Destructuring Arrays
Fixed-size arrays ([T; N]) can be destructured using square brackets [a, b, c] when the pattern matches the array's compile-time length (or uses the rest pattern ..):
fn main() {
let rgb: [u8; 3] = [255, 128, 0];
let [red, green, blue] = rgb;
println!("R={red}, G={green}, B={blue}");
}
R=255, G=128, B=0
Destructuring Structs
Named-field structs, tuple structs, and single-variant enums can also be unpacked directly in a let statement:
struct Dimensions {
width: u32,
height: u32,
}
struct Millimeters(u32);
fn main() {
let window = Dimensions {
width: 1280,
height: 720,
};
// 1. Shorthand field pattern: creates variables named `width` and `height`
let Dimensions { width, height } = window;
println!("Shorthand: {width}x{height}");
// 2. Renamed field pattern: binds `width` to `w` and `height` to `h`
let Dimensions {
width: w,
height: h,
} = window;
println!("Renamed: {w}x{h}");
// 3. Tuple struct pattern: binds the inner value to `mm`
let thickness = Millimeters(15);
let Millimeters(mm) = thickness;
println!("Thickness: {mm} mm");
}
Shorthand: 1280x720
Renamed: 1280x720
Thickness: 15 mm
Selective Mutability (mut Inside Patterns)
In Rust, mut is a property of an individual variable binding, not the entire let statement. When destructuring a compound value, you mark only the specific variables that need to be mutable with mut:
fn main() {
let (mut counter, limit) = (0, 5);
counter += 1;
// `counter` is mutable, while `limit` remains strictly immutable!
println!("counter = {counter}, limit = {limit}");
}
counter = 1, limit = 5
Writing let mut (a, b) = (1, 2); is a syntax error (mut must be placed directly before each identifier: let (mut a, mut b) = (1, 2);).
Ignoring Values with _ and ..
When you only need a subset of the elements or fields from a compound type, use _ to ignore a single position or .. (the rest pattern) to ignore multiple remaining elements or fields:
| Pattern | Meaning | Example |
|---|---|---|
_ |
Ignores exactly one element or field without binding or moving it | let (x, _, z) = (1, 2, 3); |
.. (in tuple/array) |
Skips zero or more elements at the start, middle, or end | let [first, .., last] = [1, 2, 3, 4]; |
.. (in struct) |
Ignores all unmentioned struct fields | let Config { host, .. } = cfg; |
struct ServerConfig {
host: &'static str,
port: u16,
max_connections: usize,
timeout_secs: u64,
}
fn main() {
let scores = [10, 20, 30, 40, 50];
// Extract the first and last elements, ignoring everything in between
let [first, .., last] = scores;
println!("first = {first}, last = {last}");
// Extract only `host` and `port`, ignoring the rest of the struct fields
let cfg = ServerConfig {
host: "127.0.0.1",
port: 8080,
max_connections: 1024,
timeout_secs: 30,
};
let ServerConfig { host, port, .. } = cfg;
println!("Listening on {host}:{port}");
}
first = 10, last = 50
Listening on 127.0.0.1:8080
Ownership and Partial Moves in Destructuring
Destructuring follows Rust's standard ownership rules for each element:
- Fields whose types implement
Copy(such asi32orbool) are copied into the new bindings. - Fields whose types do not implement
Copy(such asStringorVec<T>) are moved into the new bindings unless ignored with_or..or borrowed via a reference.
Notice that ignoring a non-Copy field with _ does not move or drop that field early; the original owner retains ownership of the ignored field!
fn main() {
let pair = (String::from("owned text"), String::from("kept in pair"));
// Move only the first element (`pair.0`); ignore `pair.1` with `_`
let (moved_first, _) = pair;
println!("moved_first = {moved_first}");
// Because `_` did not move `pair.1`, `pair.1` is still valid to read!
println!("pair.1 = {}", pair.1);
}
moved_first = owned text
pair.1 = kept in pair
If you want to borrow elements without moving them out of the compound value, destructure a reference (&pair) instead:
fn main() {
let pair = (String::from("alpha"), String::from("beta"));
// Destructuring `&pair` makes `a` and `b` have type `&String`
let (a, b) = &pair;
println!("Borrowed: {a} and {b}");
println!("Original pair is still intact: {pair:?}");
}
Borrowed: alpha and beta
Original pair is still intact: ("alpha", "beta")
Common Errors
Tuple or Array Length Mismatch
The number of elements in a tuple or array pattern (when .. is not used) must exactly match the type's length:
// ❌ does not compile
fn main() {
let triple = (1, 2, 3);
let (a, b) = triple;
}
error[E0308]: mismatched types
--> src/main.rs:3:9
|
3 | let (a, b) = triple;
| ^^^^^^ ------ this expression has type `({integer}, {integer}, {integer})`
| |
| expected a tuple with 3 elements, found one with 2 elements
|
= note: expected tuple `({integer}, {integer}, {integer})`
found tuple `(_, _)`
error: aborting due to 1 previous error
Refutable Patterns in let (E0005)
A standard let statement requires an irrefutable pattern—a pattern that is guaranteed to match every possible value of the type (such as a tuple, array, or struct). Attempting to destructure a multi-variant enum such as Option<T> or Result<T, E> in a plain let statement fails because the value might be None or Err:
// ❌ does not compile
fn main() {
let maybe_number = Some(42);
let Some(n) = maybe_number;
}
error[E0005]: refutable pattern in local binding
--> src/main.rs:3:9
|
3 | let Some(n) = maybe_number;
| ^^^^^^^ pattern `None` not covered
|
= note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant
= note: for more information, visit https://doc.rust-lang.org/book/ch19-02-refutability.html
= note: the matched value is of type `Option<i32>`
help: you might want to use `let else` to handle the variant that isn't matched
|
3 | let Some(n) = maybe_number else { todo!() };
| ++++++++++++++++
error: aborting due to 1 previous error
Use if let Some(n) = maybe_number { ... } or let Some(n) = maybe_number else { return; }; when unpacking fallible enum variants.