11.5Destructuring

Last updated June 13, 2026

The patterns from lesson 11.4 do something worth naming on its own: they take a compound value apart into its pieces. That operation is called destructuring, and it isn't limited to match. It works in let bindings, in function parameters, and on every compound type you've met: tuples, structs, and enums. Once you see how broadly patterns apply, a lot of Rust syntax you've already used turns out to have been destructuring all along.

You've destructured before

Back in lesson 4.11 you wrote this to unpack a tuple:

let (a, b) = (1, 2);

That (a, b) on the left is a pattern. It destructures the tuple, binding a to the first element and b to the second. The same let that creates a variable can carry a pattern, and when it does, it pulls the value apart. You've been using a pattern in every tuple-returning call since chapter 4 without calling it one.

Destructuring structs

A struct destructures by naming its fields in braces, which is the struct literal syntax run backwards:

struct Point {
    x: f64,
    y: f64,
}

fn main() {
    let p = Point { x: 3.0, y: 4.0 };
    let Point { x, y } = p;
    println!("x is {x}, y is {y}");
}
x is 3, y is 4

let Point { x, y } = p; binds x to p.x and y to p.y in one line. If you want different variable names, write let Point { x: a, y: b } = p; to bind a and b. And you can ignore fields you don't need: let Point { x, .. } = p; binds only x and the .. discards the rest, the same .. "and the rest" you met in struct update (lesson 10.3), here on the receiving side.

This shines in match when you only care about some fields or want to branch on a field's value:

fn describe(p: &Point) -> &str {
    match p {
        Point { x: 0.0, y: 0.0 } => "origin",
        Point { x: 0.0, .. } => "on the y-axis",
        Point { y: 0.0, .. } => "on the x-axis",
        _ => "somewhere else",
    }
}

The first arm matches only the exact origin (literal 0.0 in both fields), the next two match points sitting on an axis, and the catch-all handles the rest. The pattern tests specific field values and ignores the others, all in the shape of the struct itself.

Destructuring enums

You've been destructuring enums since lesson 11.2: Some(value), Message::Move { x, y }, Coin::Quarter(state) are all enum patterns that pull out carried data. The new idea here is just that the same patterns nest. An enum variant holding a struct, or a tuple holding an Option, comes apart in one pattern that mirrors the value's shape:

enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
}

fn area(shape: &Shape) -> f64 {
    match shape {
        Shape::Circle { radius } => 3.14159 * radius * radius,
        Shape::Rectangle { width, height } => width * height,
    }
}

Shape::Circle { radius } matches the variant and destructures its named field in one pattern. A pattern can be as deep as the value: Some(Point { x, .. }) matches an Option containing a Point and binds that point's x, skipping the rest. The pattern always looks like the value it matches, which is what makes deeply nested data readable instead of a pile of .0.1.field accesses.

Destructuring in function parameters

A function parameter is a binding, so it can be a pattern too. If a function always pulls a struct apart immediately, you can destructure right in the signature:

struct Point {
    x: f64,
    y: f64,
}

fn distance_from_origin(Point { x, y }: &Point) -> f64 {
    (x * x + y * y).sqrt()
}

fn main() {
    let p = Point { x: 3.0, y: 4.0 };
    println!("{}", distance_from_origin(&p));
}
5

The parameter Point { x, y }: &Point says "take a &Point, and immediately bind x and y to its fields." This is occasionally tidier than taking p: &Point and writing p.x, p.y inside. Use it sparingly; for most methods the &self from lesson 10.5 reads better, but it's good to recognize that the parameter slot accepts any pattern.

Key insight

Destructuring and construction are mirror images. Point { x: 3.0, y: 4.0 } builds a point from its parts; let Point { x, y } = p; takes one apart into its parts. The same syntax, read in two directions. Wherever Rust binds a name (a let, a function parameter, a match arm), it accepts a pattern, and a pattern can always disassemble a value whose shape it describes.

Warning

A let pattern must match every possible value of the type, or the compiler refuses it. Destructuring a struct or tuple in let is fine, because there's only one shape. Destructuring a specific enum variant in let is not: let Some(x) = maybe; is rejected, because maybe could be None and then there's nothing to bind. For that you need match, or the if let / let else tools in lesson 11.6, which exist precisely for the "this pattern might not match" case.

Quiz time

Question #1

What does this print?

fn main() {
    let ((a, b), c) = ((1, 2), 3);
    println!("{a} {b} {c}");
}
Show solution

1 2 3. The pattern ((a, b), c) mirrors the nested tuple: a and b come from the inner (1, 2), and c from the outer 3. Patterns nest as deeply as the value.

Question #2

Destructure this struct in a single let, binding the width to w and the height to h (note the renamed bindings), and print their product.

struct Rect { width: f64, height: f64 }
let r = Rect { width: 4.0, height: 2.0 };
Show solution
let Rect { width: w, height: h } = r;
println!("{}", w * h);   // 8

width: w binds the width field to a new name w; same for h. Without renaming, let Rect { width, height } = r; would bind variables named width and height.

Question #3

Why is let Some(x) = get_value(); refused, while let (a, b) = get_pair(); is fine?

Show solution

A let pattern must cover every possible value of the type. get_pair() returns a tuple, which has exactly one shape, so (a, b) always matches. get_value() returns an Option, which can be None, and Some(x) doesn't match None, leaving x unbound. A let can't have an "unmatched" case, so the compiler refuses it and points you toward match, if let, or let else.

That warning is the perfect setup for the next lesson, which introduces three tools for exactly the "a pattern that might not match" situation: if let, while let, and let else, each a lighter alternative to a full match.