Destructuring Structs and Tuples
Destructuring is how Rust lets you unpack a compound value—a tuple, a struct, or a reference to one—into its individual parts in a single step, binding each part to its own name (or discarding it) instead of reaching into the value field by field with dot notation. It is not a separate feature bolted onto structs and tuples; it is the same pattern-matching machinery that powers match and if let, just used with the always-succeeding patterns that let requires. Once you can read a destructuring pattern fluently, you can also read match arms, function parameters, and for loop variables, because they all use the same pattern syntax.
Overview: How Destructuring Works
Think of a tuple like (30, 50) or a struct like Rectangle { width: 30, height: 50 } as a sealed box with labeled compartments. Normally you open the box and read one compartment at a time: rect.width, then rect.height. Destructuring lets you open every compartment you care about at once and hand each item a name, all in a single let statement.
A pattern like let (w, h) = dimensions; or let Rectangle { width, height } = rect; is matched against the value on the right. Because the shape of the pattern always matches the shape of a tuple or struct value (you cannot fail to match a 2-tuple against a 2-element pattern), these are called irrefutable patterns, and that is exactly what let requires. Contrast this with match, where patterns like Some(x) can fail to match (the value might be None) — those are refutable patterns, and let alone cannot use them (you would need if let or match instead).
The part that trips up newcomers is what destructuring does to ownership. When you destructure a value you own (not a reference to it), Rust moves each field out into its new binding, following the exact same rules as any other move: fields whose type implements Copy (like i32 or f64) are duplicated, and everything else (like String) is moved, which invalidates the original variable for that field. Once you destructure emp in let Employee { name, id } = emp;, the binding emp is gone — you unpacked the box and cannot use the box itself again, even though you still hold what was inside it.
You can sidestep this by destructuring a reference instead of the value itself: let Employee { name, id } = &emp;. Rust’s match ergonomics (stable since the 2018 edition) automatically adjust the bindings to references in this case — name becomes &String and id becomes &u32 — and emp itself is only borrowed, so it is still usable afterward. This is the single most useful trick for destructuring without giving up ownership.
Syntax
Destructuring patterns appear after let, as function parameters, or as arms in match/if let. The general forms:
// Tuple pattern
let (a, b, c) = some_tuple;
// Struct pattern (field shorthand — name matches field name)
let StructName { field1, field2 } = some_struct;
// Struct pattern with renaming
let StructName { field1: new_name, .. } = some_struct;
// Ignoring remaining fields/elements
let (first, ..) = some_tuple;
| Pattern piece | Meaning |
|---|---|
(a, b) |
Destructures a tuple positionally; names can be anything. |
Struct { field } |
Shorthand — binds a variable with the same name as the field. |
Struct { field: name } |
Binds the field’s value to a different variable name. |
.. |
Ignores all remaining fields (structs) or elements (tuples). Usable once per pattern. |
_ |
Ignores exactly one value without binding it. |
Destructuring a &value |
Borrows instead of moving; bound names become references. |
Examples
Example 1: Destructuring Tuples
fn main() {
let coordinates = (12, 55, 8);
let (x, y, _z) = coordinates;
println!("x = {}, y = {}", x, y);
let dimensions = (1920, 1080);
let (width, height) = dimensions;
println!("width = {}, height = {}", width, height);
}
Output:
x = 12, y = 55
width = 1920, height = 1080
The pattern (x, y, _z) matches the shape of the 3-element tuple exactly. Every position needs a name or a placeholder — using _z (rather than a plain unused name) tells the compiler and any reader that ignoring the third element is intentional, avoiding an unused-variable warning while still documenting what it was.
Example 2: Destructuring Structs, Renaming and Ignoring Fields
struct Point3D {
x: f64,
y: f64,
z: f64,
}
fn main() {
let origin_offset = Point3D { x: 1.5, y: -2.0, z: 0.0 };
let Point3D { x, y, z } = origin_offset;
println!("x = {}, y = {}, z = {}", x, y, z);
let location = Point3D { x: 10.0, y: 20.0, z: 30.0 };
let Point3D { x: lat, y: lon, .. } = location;
println!("lat = {}, lon = {}", lat, lon);
}
Output:
x = 1.5, y = -2, z = 0
lat = 10, lon = 20
The first destructure uses field shorthand: because the struct’s fields are literally named x, y, and z, writing Point3D { x, y, z } creates variables with those same names. The second destructure renames x to lat and y to lon with field: new_name syntax, and uses .. to skip z entirely — no binding for z is created, and no warning is produced, because .. explicitly declares that the rest is intentionally unused.
Example 3: Nested Destructuring Through a Reference
struct Employee {
name: String,
salary: (u32, u32),
}
fn describe(employee: &Employee) {
let Employee { name, salary: (base, bonus) } = employee;
println!("{} earns {} base + {} bonus", name, base, bonus);
}
fn main() {
let emp = Employee {
name: String::from("Priya"),
salary: (60000, 5000),
};
describe(&emp);
println!("Still usable: {}", emp.name);
}
Output:
Priya earns 60000 base + 5000 bonus
Still usable: Priya
This example nests a tuple pattern (base, bonus) inside a struct pattern, unpacking two levels in a single let. Because describe takes &Employee rather than Employee, the pattern is matched against a reference — match ergonomics kick in, so name, base, and bonus are all bound as references rather than moved-out owned values. emp is only borrowed for the duration of the call, so main can still print emp.name afterward.
How It Works Step by Step
When the compiler sees let Employee { name, salary: (base, bonus) } = employee; where employee: &Employee, it performs, in order:
- Shape check. It confirms the pattern’s shape (a struct with a
namefield and asalaryfield holding a 2-tuple) matches the type being destructured. This check happens at compile time — there is no runtime cost to picking fields apart. - Default binding mode. Because the scrutinee is a reference (
&Employee) rather than an ownedEmployee, the compiler switches to reference binding mode for everything nested inside the pattern — this is match ergonomics. Without it, you would have to write therefkeyword by hand on every nested binding. - Per-field binding. Each named position in the pattern (
name,base,bonus) becomes its own variable, of type&String,&u32, and&u32respectively, borrowed from the fields insideemployee. - Ownership bookkeeping. Since nothing was moved out — only borrowed — the compiler leaves
employee(and, back inmain,emp) fully valid and usable once the borrow’s scope ends.
When you instead destructure an owned value (let Employee { name, id } = emp;), step 2 is skipped — the default binding mode stays “by value” — so step 3 moves non-Copy fields out and copies Copy fields, and step 4 marks emp itself as consumed, because every field was taken out of it.
Common Mistakes
Mistake 1: Using a Struct After Destructuring It By Value
Destructuring an owned struct moves its non-Copy fields out, which consumes the original binding:
struct Employee {
name: String,
id: u32,
}
fn main() {
let emp = Employee { name: String::from("Alex"), id: 7 };
let Employee { name, id } = emp;
println!("{} {}", name, id);
println!("{}", emp.id);
}
error[E0382]: borrow of moved value: `emp`
(emp was moved when it was destructured on the line above)
The fix is to destructure a reference instead, so fields are borrowed rather than moved:
struct Employee {
name: String,
id: u32,
}
fn main() {
let emp = Employee { name: String::from("Alex"), id: 7 };
let Employee { name, id } = &emp;
println!("{} {}", name, id);
println!("{}", emp.id);
}
Output:
Alex 7
7
Mistake 2: Forgetting mut in the Pattern
mut is not a property of the original variable — it has to be requested on each new binding a pattern creates:
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 1, y: 2 };
let Point { x, y } = p;
x = 10;
println!("{} {}", x, y);
}
error[E0384]: cannot assign twice to immutable variable `x`
Add mut directly in front of the field name you intend to reassign:
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 1, y: 2 };
let Point { mut x, y } = p;
x = 10;
println!("{} {}", x, y);
}
Output:
10 2
Best Practices
- Destructure a reference (
&value) whenever you only need to read fields — it avoids an unnecessary move and lets the caller keep using the original value. - Use
..to ignore fields you genuinely do not need, rather than binding them to an unused name — it documents intent and needs no update if the struct gains new fields later. - Prefix an intentionally unused binding with
_(like_z) instead of leaving it unprefixed, so the compiler’s unused-variable warning does not fire. - Prefer field-shorthand names (
Point { x, y }) over renaming when the field name is already the clearest name; reservefield: new_namefor cases where the field name would collide or genuinely needs a clearer local name. - Remember
..can only appear once per pattern — it collapses one contiguous run of unnamed fields or elements, not arbitrary scattered ones. - When a function only needs a couple of fields, destructure directly in the parameter list instead of accepting the whole value and reaching in with dot notation repeatedly.
Practice Exercises
- Write a struct
Bookwith fieldstitle: String,author: String, andyear: u32. Destructure a&Bookinside a function so you can print all three fields without moving the originalBook, then print the title again frommainafterward to prove it is still usable. - Given a tuple
let reading = (98.6, "F", true);, destructure it into three named variables in oneletstatement, using_for the boolean if you don’t need it, and print the temperature and unit. - Write a struct
Circle { radius: f64, color: String }. Destructure an ownedCircleby value, movingcolorout into its ownStringvariable, and explain (in a comment) why the originalCirclebinding can no longer be used afterward.
Summary
- Destructuring unpacks a tuple or struct into named bindings in one step, using the same pattern syntax as
matchandif let. letrequires irrefutable patterns — ones guaranteed to match the value’s shape, which is always true for a tuple or struct’s own shape.- Destructuring an owned value moves non-
Copyfields out and copiesCopyfields, consuming the original binding. - Destructuring a reference (
&value) borrows fields as references instead, thanks to match ergonomics, leaving the original value usable. field: new_namerenames a bound variable;..ignores the remaining fields or tuple elements;_ignores exactly one value.- Struct and tuple patterns nest freely, so you can unpack several levels of structure in a single
let.
