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 name field and a salary field 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 owned Employee, the compiler switches to reference binding mode for everything nested inside the pattern — this is match ergonomics. Without it, you would have to write the ref keyword 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 &u32 respectively, borrowed from the fields inside employee.
  • Ownership bookkeeping. Since nothing was moved out — only borrowed — the compiler leaves employee (and, back in main, 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; reserve field: new_name for 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 Book with fields title: String, author: String, and year: u32. Destructure a &Book inside a function so you can print all three fields without moving the original Book, then print the title again from main afterward to prove it is still usable.
  • Given a tuple let reading = (98.6, "F", true);, destructure it into three named variables in one let statement, 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 owned Circle by value, moving color out into its own String variable, and explain (in a comment) why the original Circle binding 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 match and if let.
  • let requires 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-Copy fields out and copies Copy fields, consuming the original binding.
  • Destructuring a reference (&value) borrows fields as references instead, thanks to match ergonomics, leaving the original value usable.
  • field: new_name renames 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.