21.12 Destructuring Arrays, Slices, Tuples, Structs, Enums, and References

21.12.1 Arrays and Slices

fn inspect_array(arr: &[i32]) {
    match arr {
        [] => println!("Empty slice"),
        [first, .., last] => println!("First: {}, Last: {}", first, last),
        [_] => println!("One item only"),
    }
}

fn main() {
    let data = [1, 2, 3, 4, 5];
    inspect_array(&data);
}

The following example showcases more intricate uses of slice patterns. It demonstrates many features at once, including binding specific elements, ignoring some elements, and capturing the “middle” portion via @ ...

fn main() {
    let array = [1, -2, 6]; // known size, so the code below covers all cases!
    // let array = &array[0..3]; // for a slice we would have to test for `&[]` and `&[_]` as well!

    match array {
        // Binds the second and third elements to separate variables
        [0, second, third] => println!(
            "array[0] = 0, array[1] = {}, array[2] = {}",
            second, third
        ),

        // a single elements can be ignored with `_`
        [1, _, third] => println!(
            "array[0] = 1, array[2] = {} and array[1] was ignored",
            third
        ),

        // You can also bind one or two elements and ignore the rest
        [-1, second, ..] => println!(
            "array[0] = -1, array[1] = {} and all other elements were ignored",
            second
        ),

        // Store the remaining elements in a separate slice or array (type depends on the match input)
        [3, second, tail @ ..] => println!(
            "array[0] = 3, array[1] = {} and the other elements were {:?}",
            second, tail
        ),

        // Combine these patterns by binding the first and last elements
        // and storing the middle ones in a slice
        [first, middle @ .., last] => println!(
            "array[0] = {}, middle = {:?}, array[2] = {}",
            first, middle, last
        ),
    }
}

21.12.2 Key Observations

  1. Ignoring Elements: Use _ or .. to skip over parts of the array.
  2. Binding Remainder: tail @ .. or middle @ .. captures any leftover elements, letting you treat them as a separate slice (the type depends on whether you’re matching against an array or a slice).
  3. Combining Patterns: You can mix these features to match very specific shapes ([3, second, tail @ ..]) or more general ones ([first, .., last]).

This snippet illustrates just how powerful Rust’s destructuring can be for arrays (or slices). By matching partial or complete patterns, you can elegantly handle all kinds of scenarios without manually writing index operations.

21.12.3 Tuples

fn sum_tuple(pair: (i32, i32)) -> i32 {
    let (a, b) = pair;
    a + b
}

fn main() {
    println!("{}", sum_tuple((10, 20)));
}

21.12.4 Structs

struct User {
    name: String,
    active: bool,
}

fn print_user(user: User) {
    match user {
        User { name, active: true } => println!("{} is active", name),
        User { name, active: false } => println!("{} is inactive", name),
    }
}

fn main() {
    let alice = User { name: String::from("Alice"), active: true };
    print_user(alice);
}

21.12.5 Enums

Enums can be nested, and you can destructure them deeply:

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

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

fn main() {
    let c = Shape::Circle { radius: 3.0 };
    println!("Circle area: {}", area(c));
}

21.12.6 Pattern Matching With References

Rust provides multiple ways to match against references, whether those references are in an Option or directly in a variable. Consider the following examples:

fn main() {
    // Example 1: Option of a reference
    let value = Some(&42); 
    match value {
        // Matches "Some(&val)" when we stored a reference: Some(&42)
        Some(&val) => println!("Got a value by dereferencing: {}", val),
        None => println!("No value found"),
    }

    // Example 2: Matching a reference directly using "*reference"
    let reference = &10;
    match *reference {
        10 => println!("The reference points to 10"),
        _ => println!("The reference points to something else"),
    }

    // Example 3: "ref r"
    let some_value = Some(5);
    match some_value {
        Some(ref r) => println!("Got a reference to the value: {}", r),
        None => println!("No value found"),
    }

    // Example 4: "ref mut m"
    let mut mutable_value = Some(8);
    match mutable_value {
        Some(ref mut m) => {
            *m += 1;  // Modify through the mutable reference
            println!("Modified value through mutable reference: {}", m);
        }
        None => println!("No value found"),
    }
}

Key Points:

  • Direct Matching: Some(@val) matches an integer reference stored in Some(...).
  • Dereferencing: match *reference manually dereferences a reference.
  • ref and ref mut: Borrow a reference (immutable or mutable) to the inner value, preventing a move. This can be especially helpful when you want to avoid transferring ownership or when you need to modify data in place.