21.11 Destructuring Data Structures

A major strength of patterns is destructuring: breaking down composite types into their constituent parts.

21.11.1 Tuples

fn process_3d_point(point: (i32, i32, i32)) {
    match point {
        (0, 0, 0) => println!("At the origin"),
        (x, 0, 0) => println!("On X-axis at {}", x),
        (0, y, 0) => println!("On Y-axis at {}", y),
        (0, 0, z) => println!("On Z-axis at {}", z),
        (x, y, z) => println!("General point at ({}, {}, {})", x, y, z),
    }
}

fn main() {
    process_3d_point((5, 0, 0)); // Output: On X-axis at 5
    process_3d_point((0, -2, 0)); // Output: On Y-axis at -2
    process_3d_point((1, 2, 3));  // Output: General point at (1, 2, 3)
}

21.11.2 Structs

Use field names to destructure. Field name punning ({ field } for { field: field }) is common.

struct User {
    id: u64,
    name: String,
    is_admin: bool,
}

fn describe_user(user: &User) {
    match user {
        // Use punning for name, specify is_admin, ignore id with `..`
        User { name, is_admin: true, .. } => {
            println!("Admin user: {}", name);
        }
        // Use specific id, pun name, specify is_admin
        User { id: 0, name, is_admin: false } => {
            println!("Special guest user (ID 0): {}", name);
        }
        // Use punning for name, ignore other fields
        User { name, .. } => {
            println!("Regular user: {}", name);
        }
    }
}

fn main() {
    let admin = User { id: 1, name: "Alice".to_string(), is_admin: true };
    let guest = User { id: 0, name: "Guest".to_string(), is_admin: false };
    let regular = User { id: 2, name: "Bob".to_string(), is_admin: false };

    describe_user(&admin);   // Output: Admin user: Alice
    describe_user(&guest);   // Output: Special guest user (ID 0): Guest
    describe_user(&regular); // Output: Regular user: Bob
}

21.11.3 Arrays and Slices

Match fixed-size arrays or variable-length slices by elements.

fn analyze_slice(data: &[u8]) {
    match data {
        [] => println!("Empty slice"),
        [0] => println!("Slice contains only 0"),
        [1, x, y] => println!("Slice starts with 1, followed by {}, {}", x, y),
        // Match first element, ignore middle (`..`), bind last
        [first, .., last] => {
            println!("Slice starts with {} and ends with {}", first, last);
        }
         // Match fixed prefix [0, 1], capture the rest in `tail`
        [0, 1, tail @ ..] => {
             println!("Slice starts [0, 1], rest is {:?}", tail);
        }
        // Fallback using wildcard `_`
        _ => println!("Slice has {} elements, didn't match specific patterns", data.len()),
    }
}

fn main() {
    analyze_slice(&[]);          // Output: Empty slice
    analyze_slice(&[0]);         // Output: Slice contains only 0
    analyze_slice(&[1, 5, 8]);   // Output: Slice starts with 1, followed by 5, 8
    analyze_slice(&[10, 20, 30, 40]); // Output: Slice starts with 10 and ends with 40
    analyze_slice(&[0, 1, 2, 3]); // Output: Slice starts [0, 1], rest is [2, 3]
    analyze_slice(&[2, 3]);      // Output: Slice has 2 elements...
}

Key slice/array patterns:

  • [a, b, c]: Matches exactly 3 elements.
  • [head, ..]: Matches 1 or more elements, binds head.
  • [.., tail]: Matches 1 or more elements, binds tail.
  • [first, .., last]: Matches 2 or more elements.
  • [prefix.., name @ .., suffix..]: Captures sub-slices.

21.11.4 Matching References and Using ref/ref mut

When matching references or needing to borrow within a pattern (to avoid moving values), use &, ref, and ref mut.

  1. & in Pattern: Matches a value held within a reference.
  2. ref Keyword: Creates an immutable reference (&T) to a field or element within the matched value. Use this when matching by value but need to borrow parts instead of moving them.
  3. ref mut Keyword: Creates a mutable reference (&mut T). Use this when matching by value or mutable reference and need mutable access to parts without moving.
fn main() {
    // 1. Matching `&` directly
    let reference_to_val: &i32 = &10;
    match reference_to_val {
        &10 => println!("Value is 10 (matched via &)"), // `&10` matches `&i32`
        _ => {}
    }

    // Example with Option<&T>
    let opt_ref: Option<&String> = Some(&"hello".to_string());
    match opt_ref {
        Some(&ref s) => println!("Got reference to string: {}", s), // `&ref s` matches `&String`, `s` is &String
        None => {}
    }


    // 2. Using `ref` to borrow from an owned value being matched
    let maybe_owned_string: Option<String> = Some("world".to_string());
    match maybe_owned_string {
        // `ref s` makes `s` an `&String`, borrowing from `maybe_owned_string`
        Some(ref s) => {
            println!("Borrowed string: {}", s);
            // `maybe_owned_string` is still owned outside the match, because `s` only borrows
        }
        None => {}
    }
     // We can still use maybe_owned_string here if it wasn't None
     if let Some(s) = maybe_owned_string {
         println!("Original Option still contains: {}", s);
     }


    // 3. Using `ref mut` to modify through a mutable reference
    let mut maybe_count: Option<u32> = Some(5);
    match maybe_count {
         // `ref mut c` makes `c` an `&mut u32`, mutably borrowing
         Some(ref mut c) => {
             *c += 1;
             println!("Incremented count: {}", c);
         }
         None => {}
    }
    println!("Final count: {:?}", maybe_count); // Output: Final count: Some(6)
}

Using ref and ref mut is essential when destructuring non-Copy types (like String, Vec) if you don’t want the pattern matching to take ownership of those parts.