This post is still being written.

Rust Dereferencing vs Destructuring — For the Kids

Thanks Chat GPT


TL;DR

  • Dereferencing: accessing the value behind a reference or smart pointer (e.g., *x, or implicit via Deref). Used to read or mutate the underlying data, respecting Rust’s borrowing rules (&T, &mut T).
  • Destructuring: breaking apart composite values (tuples, structs, enums) using pattern matching syntax. Can move or borrow parts depending on context.

Table of Contents


Introduction

If you’re learning Rust and the concepts of ownership, borrowing, and references still feel unfamiliar or intimidating — you’re not alone.

Coming from languages like Python or C++, it’s easy to assume that Rust’s &, *, and smart pointers behave the same way. But Rust has its own philosophy, built around memory safety and enforced by strict compile-time rules.

This article aims to clarify the difference between dereferencing and destructuring — two concepts that are often confused, especially outside of match expressions.

Why the Confusion?

At first glance, dereferencing (using *) and destructuring (in let, if let, or match patterns) can look similar when working with references. Consider the following lines:

let r = &Some(5);

if let Some(val) = r {
    println!("val = {val}");
}

No explicit *r — yet the pattern matches. How?

Now look at this one-liner:

let Some(x) = &Some(42);

Is this dereferencing, destructuring, or both?

And then:

let b = Box::new((42, "hello"));
let (x, y) = *b;
let (x, y) = b; // Doesn't compile

All three examples seem simple — but do you really understand why they behave this way?

If you already know the answers, maybe this article isn’t for you. But if you’ve ever hesitated, been surprised by a compilation error, or struggled to explain why one line works and another doesn’t… then you’re in the right place.

This article won’t just define dereferencing and destructuring — it will show you how Rust treats them, how the compiler helps (or confuses) you, and when the distinction truly matters.

What This Article Covers

  1. Dereferencing: How to access values through references and smart pointers (Box, Rc, RefCell), and how mutability affects this.

  2. Destructuring: How to unpack values in let, match, and function or closure parameters — including when working with references.

No multithreading knowledge required. For a threaded use case, see this post on Multithreaded Mandelbrot sets (in French).

Whether you’re just starting with Rust or adjusting your mental model, this post is for you.


Dereferencing: A smooth start

Copy and paste the code below in the Rust Playground then hit CTRL+ ENTER

With the other sample code I will only show the function of interest, not the main() function.

fn dereferencing01() {
    println!("\nDereferencing 01 : 101");

    let my_value = 5; // => my_value: i32
    println!("my_value : {}", my_value);

    let addr_of_my_value = &my_value; // => addr_of_my_value: &i32
    println!("addr_of_my_value : {:p}", addr_of_my_value);

    let content_at_addr_of_my_value = *addr_of_my_value; // content_at_addr_of_my_value => i32
    println!("content_at_addr_of_my_value : {}", content_at_addr_of_my_value);
}

fn main(){
    dereferencing01();
}

Expected output

Dereferencing 01 : 101
my_value : 5
addr_of_my_value : 0x7ffeac669fa4
content_at_addr_of_my_value : 5

Explanations

  • my_value is a binding (a variable) of type i32 and it has the value 5. If you are not sure about the difference between binding and variable keep the term variable in mind for no, but put your hand on your heart and swear you will read this page in English.
  • Next come the interesting part. addr_of_my_value is a reference to my_value. Its value is the memory address of my_value. I hope you already know that every variable, data structure, code… Is somewhere in the memory of the PC. So they all have a memory address. The syntax is what it is and the &my_value means, in plain English : address of my_value

Ah OK. So a reference is a pointer. Right? Almost because they are both used to indirectly access and manipulate objects but no because they have key differences. Since there is no pointer in Rust (I know *const T and *mut T but let’s avoid them for today), let’s illustrate de differences between pointers and references using C++. No worries. Syntax is very similar.

References and Pointers comparaison in C++

1. Syntax and Declaration

  • Pointer: Declared with *, can be reassigned to point to different objects (or nullptr).
    int x = 10;
    int* ptr = &x;  // ptr is a pointer pointing to x
    int y = 20;
    ptr = &y;       // Now pointer is pointing to y
    
  • Reference: Declared with &, must be initialized upon declaration and cannot be reassigned.
    int x = 10;
    int& ref = x;     // ref is an alias, another name, for x
    int y = 20;
    ref = y;          // Doesn't make ref point to y; instead, assigns y's value to x because ref is an alias of x
    

2. Nullability

  • Pointer: Can be nullptr (uninitialized or explicitly set to null).
    int* ptr = nullptr;  // Valid
    
  • Reference: Must always refer to a valid object (no null references).
    int& ref;  // Does'nt compile: Must be initialized
    

3. Memory Address Handling

  • Pointer: Stores the memory address of an object. You can perform arithmetic (e.g., ptr++).
    int arr[3] = {1, 2, 3};
    int* ptr = arr;
    ptr++;  // Moves to next element
    
  • Reference: Acts as an alias (another name) for an existing variable. No arithmetic.
    int x = 10;
    int& ref = x; // ref is just another name for x; no address manipulation.
    

4. Indirection & Dereferencing

  • Pointer: Requires explicit dereferencing (*) to access the value.
    int x = 10;
    int* ptr = &x;
    cout << *ptr;  // Outputs 10
    
  • Reference: Automatically dereferenced (no need for *).
    int x = 10;
    int& ref = x;
    cout << ref;  // no * needed. ref acts as an alias 
    

5. Safety

  • Pointer: Risk of dangling pointers (pointing to freed memory).
  • Reference: Safer (cannot be null, always bound to an object).

Summary Table

Feature Pointer (*) Reference (&)
Syntax int* ptr = &x; int& ref = x;
Reassignable? Yes No
Can be null? Yes No
Memory arithmetic Yes No
Dereferencing Explicit dereferencing cout << *ptr; No cout << ref;

To keep in mind in a Rust context

  • Syntax
    • let ref_to_my_value = &my_value;
    • let content_ref = *ref_to_my_value;
  • Not dangling. A reference is bound to an object
  • Reassignable if &mut, not reassignable otherwise
  • No arithmetic on a reference

Dereferencing: Mutability

Let’s run the code below.

fn dereferencing02() {
    // --------------------------------------------
    println!("\nDereferencing 02 : mutability\n");
    println!("\n\n1 - Mutability of the referenced variable");
    let my_value = 5; // immutable variable
    println!("my_value : {}", my_value);
    let ref_to_my_value = &my_value; // immutable reference to immutable variable
    println!("ref_to_my_value : {}", ref_to_my_value);
    println!();

    // *ref_to_my_value = 24; // => does not compile: `ref_to_my_value` is a `&` reference, so the data it refers to cannot be written

    let mut my_mutable_value = 55; // mutable variable
    println!("my_mutable_value : {}", my_mutable_value);
    let ref_to_my_mutable_value = &mut my_mutable_value; // mutable reference to mutable value
    println!("ref_to_my_mutable_value : {}", ref_to_my_mutable_value);
    println!();
    *ref_to_my_mutable_value += 1;
    println!("ref_to_my_mutable_value : {}", ref_to_my_mutable_value);
    println!("my_mutable_value : {}", my_mutable_value);
    println!();

    // --------------------------------------------
    println!("\n\n2- Mutability of the reference");
    let my_value = 5; // immutable variable
    println!("my_value : {}", my_value);
    let other_value = 42;
    println!("other_value : {}", other_value);
    println!();
    let ref_to_my_value = &my_value; // immutable reference to immutable variable
    println!("ref_to_my_value : {}", ref_to_my_value);
    println!();

    // ref_to_my_value = &other_value; // => does not compile: cannot assign twice to immutable variable `ref_to_my_value`
    
    let ref_to_my_value = &other_value; // => shadowing. Does compile
    println!("ref_to_my_value : {}", ref_to_my_value); // => ref_to_my_value: &i32
    println!();
    let mut mut_ref_to_my_value = &my_value; // mutable reference to immutable variable
    println!("mut_ref_to_my_value : {}", mut_ref_to_my_value);
    mut_ref_to_my_value = &other_value; // mut_ref_to_my_value now reference other_value
    println!("mut_ref_to_my_value : {}", mut_ref_to_my_value);
    let other_value = std::f64::consts::PI; // => other_value: f64
    println!("other_value : {}", other_value);

    // mut_ref_to_my_value = &other_value; // => does not compile: expected `&{integer}`, found `&f64`
}

Expected output

Dereferencing 02 : mutability
1 - Mutability of the referenced variable
my_value : 5
ref_to_my_value : 5
my_mutable_value : 55
ref_to_my_mutable_value : 55
ref_to_my_mutable_value : 56
my_mutable_value : 56

2- Mutability of the reference
my_value : 5
other_value : 42
ref_to_my_value : 5
ref_to_my_value : 42
mut_ref_to_my_value : 5
mut_ref_to_my_value : 42
other_value : 3.141592653589793

Explanations

Just to make sure we are on the same page. There are 2 kinds of mutability to consider here :

  1. We want the reference to point to a mutable variable : mutability of the referenced variable
  2. We want the reference to be able to point to different variables (of the same type) : mutability of the reference

If you don’t feel confident enough to explain this concept to your kids, read this page about Mutability.

In the first part of the code above we focus on the mutability of the referenced variable

  • Like in the first code snippet, we create a variable (immutable variable, let my_value = 5;) and print its content
  • Then we create an immutable reference to the immutable variable (let ref_to_my_value = &my_value;) and print its content
  • The commented line does not compile (*ref_to_my_value = 24;). With this setup we cannot mutate the content of the reference (and the variable)

Here’s how we can address this issue

  • We create and print a mutable variable (let mut my_mutable_value = 55;)
  • Then create (let ref_to_my_mutable_value = &mut my_mutable_value;) and print the content of a mutable reference to a mutable variable
  • We mutate the content of the reference (*ref_to_my_mutable_value += 1;)
  • Finally we print both, the content of the reference and the variable

Now, in the second part of the code we look at the mutability of the reference itself. We want the reference being able to “point to” different variables.

  • We create 2 immutable variables (let my_value = 5;, let other_value = 42;)
  • We create an immutable reference to an immutable variable (let ref_to_my_value = &my_value;)
  • The commented line does not compile (ref_to_my_value = &other_value;). Indeed the reference is immutable. It cannot mutate, it cannot “point to” another variable.

Here’s what we can do

  • We could “write over” (shadowing) the previous reference (let ref_to_my_value = &other_value;)

Here’s another option

  • We create a mutable reference to an immutable variable (let mut mut_ref_to_my_value = &my_value;)
  • The reference can then be mutated and “point to” another variable (let mut mut_ref_to_my_value = &my_value; then mut_ref_to_my_value = &other_value;)
  • A mutable reference can only reference variable of the same type. The first version of other_value was an i32. Now if we create a f64 version of other_value (let other_value = std::f64::consts::PI;), mut_ref_to_my_value cannot point to it. The very last line does not compile.

That is fine but why should I care? I mean, what is the purpose? One of the key usage of references is to pass efficiently arguments to functions. Let’s see how it works now.

Dereferencing: Reference as argument

fn dereferencing03() {
    println!("\nDereferencing 03 : ref as argument\n");
    fn my_function01(v: Vec<i32>) {
        println!("{:?}", v);
    }
    fn my_function02(v: &Vec<i32>) {
        println!("{:?}", *v);
        println!("{:?}", v); // deref coercion in action
    }
    fn my_function03(v: &[i32]) {   // accept reference to vectors or arrays
        // println!("{:?}", *v);    // Does not compile because *v is of type [i32] with no Sized trait (expected by println!)
                                    // Only references like `&[i32]` implement the `Debug` trait; `[i32]` alone doesn't, as it's dynamically sized)
        println!("{:?}", &*v);      // Overkill ?
        println!("{:?}", v);
    }
    let my_vector = vec![42, 43, 44];
    my_function01(my_vector); // after the call my_vector disappears

    // println!("{:?}", my_vector); // Does not compile

    let my_vector = vec![42, 43, 44]; // must recreate my_vector
    my_function02(&my_vector); // pass a reference
    my_function03(&my_vector);
    let my_array = [142, 143, 144]; // an array on the stack
    my_function03(&my_array);
}

Expected output

Dereferencing 03 : ref as argument
[42, 43, 44]
[42, 43, 44]
[42, 43, 44]
[42, 43, 44]
[42, 43, 44]
[142, 143, 144]
[142, 143, 144]

Explanations

  • Yes we can! Yes we can have function definitions inside function definition
  • my_function01 has a unique parameter of type vector of i32
  • my_function02 has a unique parameter of type reference to a vector of i32
  • my_function03 has a unique parameter of type slice (&[T]) of i32

Pass by value

Then we create my_vector (let my_vector = vec![42, 43, 44];), a vector of i32 and we give it as an argument to my_function01

  • The argument is passed by value
  • So it is given to the function and we lost it (RIP)
  • After the call to my_function01 we cannot print my_vector

Before to move forward we must recreate my_vector (let my_vector = vec![42, 43, 44];)

Pass by reference

The 2 calls to my_function02 and my_function03 are much more interesting

  • In both calls my_vector is passed by reference (my_function02(&my_vector);)
  • my_vector is borrowed to the functions
  • It is not given, it does not disappear after the call
  • This is why my_vector is still available after the first call and can be reused in the second (my_function03(&my_vector);)

This is all good… But why do we do this? What I will say here is not 100% accurate but bear with me because the idea is to get the concept, then I’ll tell you the truth. I promise.

  • In the call to my_function01 we passed the vector by value. It is like passing the 3 values. We push them on the stack. Call the function. In the function, we pop the values from the stack, then work on them. It was OK because we only had 3 i32. What if we had 1_000_000? This would be inefficient.
  • In the calls to my_function02 and my_function03 we pass a reference, the address of the my_vector. No matter the length of the vector we will always use 8 bytes (on a 64 bit OS, a memory address is on 8 bytes). This is much smarter and much faster.

OK… But what is going on in my_function02?

  • In my_function02, v is a parameter of type reference to a vector of i32 (&Vec<i32>). If we want to get access to its content, we must dereference it. This is why the first println! has *v as an argument (println!("{:?}", *v);).
  • However, like in C++, in the second call to println! we can use v as a parameter (println!("{:?}", v);). The Rust compiler will then apply deref coercion, to transform, on the fly, the &Vec<i32> (v) into a Vec<i32> (*v). How does this work here ?
    • println! expects a type which implement Debug trait but &Vec<i32> doesn’t
    • The compiler looks for Deref trait implementations on &Vec<i32>
    • Vec<T> implements Deref<Target = [T]>, meaning &Vec<i32> can auto-convert to &[i32] (a slice of i32)
    • And &[i32] implements the Debug trait, so the reference to a vector of i32 is printed as a slice of i32.

And what about my_function03?

  • In my_function03, v parameter is a slice of i32 (&[i32])
  • The commented println! does not compile (println!("{:?}", *v);). Indeed, *v is of type [i32] and this type does not implement the Sized trait expected by println!
  • If we really want to dereference the parameter then we can use the second println! but it looks weird (println!("{:?}", &*v);)
  • The third println! is the way to go (println!("{:?}", v);)

  • Last but not least, we create an array (my_array) and pass a reference to it as an argument to my_function03 (my_function03(&my_array);)
    • This demonstrate that my_function03 works fine when it receives as a parameter either a reference to vector or an array (thanks to deref coercion).

You promised to tell the truth about what happens when you pass a vector by value… In fact, the content of a vector is allocated on the heap ([42, 43, 44]) and you can view a vector as a struct with 3 fields (PLC) :

  1. address of the data on the heap (P, pointer)
  2. the len of the vector (L, len)
  3. the capacity of the vector (C, capacity >=len)

So, yes, I lied. When we pass by value a vector of 100 i32 we do not passe 100 values. We only pass 3 i64(address, len and capacity). Nevertheless what is wrong for a vector is true for an array. An array is just a set of contiguous memory addresses on the stack and if we pass an array by value we pass its content by value.


You mentioned data on the heap. How to dereference this kind of data ? You read my mind this is what we will focus on now.

Dereferencing: Allocations on the heap

fn dereferencing04() {
    println!("\nDereferencing 04 : Box, Rc and Deref\n");
    // Function that takes a value by reference
    fn print_ref(v: &i32) {
        println!("Value: {}", *v);
        println!("Value: {}", v);
    }
    // Function that takes a Box<i32>
    fn print_box(v: Box<i32>) {
        println!("Boxed value: {}", v);
    }
    // Create a value on the heap using Box
    let b = Box::new(123);
    println!("Address of the heap in the Box : {:p}", b);
    println!("Address of b on the stack      : {:p}", &b);
    // We can dereference Box<T> directly thanks to Deref
    println!("Dereferenced Box: {}", *b);
    // The function expects &i32, but we give it &Box<i32>
    // Thanks to deref coercion, this works
    print_ref(&b);
    // Can also pass the Box directly if signature matches
    print_box(b); // b is moved here
}

Expected output

Dereferencing 04 : Box, Rc and Deref
Address of the heap in the Box : 0x5e603cc1fb10
Address of b on the stack      : 0x7ffc6dbbec38
Dereferenced Box: 123
Value: 123
Boxed value: 123

Explanations

  • We first define 2 functions print_ref and print_box
  • Then, in order to allocate memory on the heap we use Box::new() (let b = Box::new(123);)
  • Let’s keep in mind this create a unique pointer that own the pointed area.
  • Here the required space to store the value 123 (an i32, 4 bytes) is allocated on the heap
  • b a variable of type Box<i32> remains on the stack. b is a smart pointer which implements RAII
  • RAII = Ressource Acquisition Is Initialisation. This term is pretty well known in C++. This creates a wrapper around the allocated memory and warranty that the memory will be automatically freed when b goes out of scope (even if a panic or an early return happens)
  • Once the variable b exists, with a “simple” println! we print, the address on the heap which is in the Box (println!("Address of the heap in the Box : {:p}", b);).
  • Just to make sure we understand that the address of b (data type Box<i32>) on the stack has nothing to do with the address in the boxe (pointing to the heap) we print the address of b so that we can compare (println!("Address of b on the stack : {:p}", &b);).
  • Next we dereference the boxe b and print the value it points to (println!("Dereferenced Box: {}", *b);). This is really cool because once the boxe is created, we use b has a regular reference to an i32.
  • In the call to print_ref we pass a reference to the box (print_ref(&b);). The box is borrowed and it remains available after the call.
  • In the print_ref function the first println!("Value: {}", *v); is what we should write but the deref coercion allow us to write the second (println!("Value: {}", v);)
  • In the last call, make sure to understand that b (a variable on the stack) is moved and no longer available right after the call to print_box. This is at this point that RAII mechanism will work behind the scene and deallocate the memory on the heap.

Ok. Now I know how to safely allocate memory on the heap. But can I have 2 boxes pointing to the same area? I know what you mean. The heap allocated memory could be a picture of your brand new Aprilia RSV4 and you would like to make sure your friends can look at it without modifying it. This is not possible with a box directly. So let’s find a solution…

Dereferencing: Rc<T> and Reference Count

Indeed, in order to manage memory efficiently we need to be smarter than a box and to include a counter in order to know how many people are currently watching the picture of your motorbike. Let’s look at the code below :

use std::rc::Rc;
fn dereferencing05() {
    println!("\nDereferencing 05 : Rc<T> and Reference Count\n");
    // Function that takes Rc<i32>
    fn print_rc(v: &Rc<i32>) {
        println!("From print_rc : {}", v);
    }
    // Create an Rc pointing to a value on the heap
    let rc1 = Rc::new(999);
    println!("Initial value: {}", rc1); 
    println!("Address in Rc: {:p}", Rc::as_ptr(&rc1));
    println!("Reference count after creation: {}", Rc::strong_count(&rc1)); // 1
    print_rc(&rc1);
    // Create a clone of rc1 — this does not copy the value
    let rc2 = Rc::clone(&rc1);
    println!("\nAfter cloning rc1 into rc2:");
    println!("rc1 points to: {}", rc1);
    println!("rc2 points to: {}", rc2);
    println!("Reference count (rc1): {}", Rc::strong_count(&rc1)); // 2
    println!("Reference count (rc2): {}", Rc::strong_count(&rc2)); // 2
    {
        // Introduce a new scope
        let rc3 = Rc::clone(&rc2);
        println!("\nInside inner scope with rc3:");
        println!("rc3 points to: {}", rc3);
        println!("Reference count: {}", Rc::strong_count(&rc3)); // 3
    } // rc3 goes out of scope here
    println!("\nAfter rc3 is dropped:");
    println!("Reference count (rc1): {}", Rc::strong_count(&rc1)); // 2
}

Expected output

Dereferencing 05 : Rc<T> and Reference Count
Initial value: 999
Address in Rc: 0x56d9e7fceb20
Reference count after creation: 1
From print_rc : 999

After cloning rc1 into rc2:
rc1 points to: 999
rc2 points to: 999
Reference count (rc1): 2
Reference count (rc2): 2

Inside inner scope with rc3:
rc3 points to: 999
Reference count: 3

After rc3 is dropped:
Reference count (rc1): 2

Explanations

  • We first create a reference counting pointer, pointing to the memory on the heap (let rc1 = Rc::new(999);)
  • Again, just to make sure we are in sync : Rc is for single-threaded only
  • This said, we print the value and the address on the heap. I really don’t like the fact that Box and Rc don’t have similar API. For example why I can’t write : ```rust use std::rc::Rc;

fn main(){ let b = Box::new(999); println!(“Dereferenced Box: {}”, b); // 999 println!(“Dereferenced Box: {}”, *b); // 999 println!(“Address in the Box : {:p}”, b); // 0x5eca24b8eb10 // println!(“Address in the Box : {:p}”, Box::as_ptr(&b)); // Does’nt compile : use of unstable library feature box_as_ptr

println!();

let rc1 = Rc::new(999);
println!("Initial value: {}", rc1); // 999
println!("Initial value: {}", *rc1); // 999
println!("Initial value: {:p}", rc1); // 0x5eca24b8eb40
println!("Address in Rc: {:p}", Rc::as_ptr(&rc1)); // 0x5eca24b8eb40 } ``` * Finally we demonstrate how to dereference an Rc when we call `print_rc(&rc1);`. Nothing fundamentally new here.

Then it becomes interesting…

  • We print the value of the counter of the smart pointer (println!("Reference count after creation: {}", Rc::strong_count(&rc1)); // 1). At this point, only one pointer is pointing to area where 999 is stored

Then it becomes really interesting…

  • Indeed we clone the smart pointer we had (let rc2 = Rc::clone(&rc1);). It is important to keep in mind that no copy or worst, no deep copy happens. When applied to a reference-counted smart pointer, .clone() simply add 1 to the counter. The .clone() operation is fast and cheap.
  • Once rc2 is created we print the value it points to and the current value of the reference counter (println!("Reference count (rc2): {}", Rc::strong_count(&rc2)); // 2). This should not be a surprise but the counter of rc1 and rc2 are equal (otherwise we would be in be trouble)

In a last experiment we create a scope ({ and }) where we create another clone (let rc3 = Rc::clone(&rc2);).

  • Before the end of the scope we display the counter of one of the clones (println!("Reference count: {}", Rc::strong_count(&rc3)); // 3)
  • After the scope we display the counter of one of the clones (println!("Reference count (rc1): {}", Rc::strong_count(&rc1)); // 2). It goes from 3 to 2 since rc3 no longer exists.

Well, well, well… I know you will NOT talk about references in a multithreaded context but… What if I need to mutate the heap allocated memory ? This could be the case where the allocated memory represents your bank account where your company transfer your salary and where you would like to check the total amount available. To do so, we need to be even smarter than before…

Dereferencing: Rc<RefCell<T>> for shared mutation (single-thread)

However, the good news it that instead of learning a new smart pointer, we will reuse what we already know about Rc (reference-counted smart pointer) and add interior mutability to the heap allocated memory. This is done using RefCell. First thing first, let’s run the code below :

use std::cell::RefCell;
use std::rc::Rc;
fn dereferencing06() {
    println!("\nDereferencing 06 : Rc<RefCell<T>> for shared mutation (single-thread)\n");
    // Rc enables multiple ownership, RefCell enables interior mutability
    let shared_vec = Rc::new(RefCell::new(vec![1, 2, 3]));
    println!("shared_vec: {:?}", shared_vec);
    println!("Reference count: {}", Rc::strong_count(&shared_vec));
    // Clone the Rc to get multiple owners
    let a = Rc::clone(&shared_vec);
    let b = Rc::clone(&shared_vec);
    println!("Reference count: {}", Rc::strong_count(&shared_vec)); // 3
    // Mutate the shared vector from owner `a`
    {
        let mut vec_ref = a.borrow_mut(); // borrow `a` as mutable
        vec_ref.push(4);
        println!("a pushed 4: {:?}", vec_ref);
    }
    // Read from the shared vector via owner `b`
    {
        let vec_ref = b.borrow(); // borrow `b` as immutable
        println!("b sees the vector: {:?}", vec_ref);
    }
    // Shows that the compiler doesn't see borrow conflicts, but the runtime does.
    {
        let _first = shared_vec.borrow_mut();
        // let _second = shared_vec.borrow_mut(); // panics at runtime
    }
    // Reference count stays at 3 until `a`, `b`, and `shared_vec` go out of scope
    println!("Reference count: {}", Rc::strong_count(&shared_vec)); // 3
}


Expected output

Dereferencing 06 : Rc<RefCell<T>> for shared mutation (single-thread)
shared_vec: RefCell { value: [1, 2, 3] }
Reference count: 1
Reference count: 3
a pushed 4: [1, 2, 3, 4]
b sees the vector: [1, 2, 3, 4]
Reference count: 3

Explanations

  • Let’s say we want to share a vector shared_vec ofi32
  • We would write : let shared_vec = Rc::new(vec![1, 2, 3]);
  • Since we want to add interior mutability (ability to mutate the content of the vector) we write : let shared_vec = Rc::new(RefCell::new(vec![1, 2, 3]));
  • Everthing we said about cloning and inspecting the counter remains valid. After all, shared_vec is a Rc<Vec<i32>>
  • This is true but shared_vec is a Rc<RefCell<Vec<i32>>> and this is what allow us to mutate the heap allocated memory safely (almost)

Then we create the first scope (more on this in few lines)

  • Inside the scope, we create vec_ref (let mut vec_ref = a.borrow_mut();). To make a long story short it borrow the wrapped object (vec![1, 2, 3]) with the ability to mutate it (.borrow_mut()).
  • Then we push a value in the borrowed vector (vec_ref.push(4);) and print it
  • And the scope ends right after the } vec_ref goes out of scope and no one is borrowing the heap allocated vector

We then create a second scope

  • We create a new version vec_ref (let vec_ref = b.borrow();). This time it borrows b without the ability to mutate it (.borrow() not .borrow_mut()).
  • The print shows that the mutably shared vector has been mutated

Why do we need scopes here ? This is the one million dollars question… And you should have the answer. Make a test. Remove them and the code will panic on line let vec_ref = b.borrow();. Can you say why?

Without the scopes, when we reach the line let vec_ref = b.borrow();, on stage we have one borrower with mutability capabilities and we would like to add a borrower with read-only capabilities. No way. You know the rule : Only one writer or multiple readers.

One thing to keep in mind. The conflict among borrowers happen at runtime not at compile time

This is what is demonstrated in the last scope.

  • If you uncomment the second line // let _second = shared_vec...
  • The code panic at runtime

Any tips and trick to share ? Here are a few common traps and surprises you might encounter (I did)

Rust Gotchas: Dereferencing Edition

  1. References are not pointers (exactly)
    They behave similarly but are not the same. You can’t do pointer arithmetic, and they must always be valid and non-null.

  2. &mut vs mut
    • mut x: you’re allowed to modify x.
    • &mut x: you’re allowed to modify the value x points to (&mut is a compound operator in Rust, it is a single “logical keyword”, which reads “mutable reference to”)
    • But let mut x = &y; only means that x (the reference) can change to point elsewhere — not that y is mutable!
  3. Shadowing vs reassignment
    You can “reassign” an immutable reference via shadowing (let x = &y; again), but trying to reassign without let won’t compile.

  4. Boxing isn’t cloning
    Box::new(value) allocates value on the heap. It does not create a deep copy when passed by value — it moves ownership unless you explicitly .clone() the inner value.

  5. **Rc cloning doesn’t clone the value** It just increments the reference counter. Great for sharing read-only access — but not safe across threads or for mutation without `RefCell`.

  6. Deref coercion looks like magic
    But it’s not: it follows well-defined Deref rules. Still, don’t rely on it blindly — sometimes explicit * helps readability and prevents surprises.

  7. Borrow checker checks borrows at compile time… until RefCell enters the game
    RefCell<T> defers checks to runtime. That means your code compiles, but can still panic if you violate borrow rules (e.g., two mutable borrows).

Now that we’ve seen how to follow pointers, it’s time to open the box and peek inside with destructuring!

Patterns and Destructuring Patterns in Rust

What is Destructuring? Destructuring is the act of using a pattern to break a value apart and extract its inner pieces. As we will see, we are not just assigning a value, we are unpacking it. However before diving into destructuring, it’s important to understand what a pattern is.

In Rust, a pattern is a syntax that matches the shape of a value. You’ve probably already seen patterns in match statements, if let, or while let — but patterns are everywhere: in let bindings, function and closure parameters, and for loops.

OK… But what is a pattern?

A pattern tells the compiler: “I expect a value of a certain shape — and I want to extract pieces from it.” Ok, let’s not waste time and go and see some code.


Destructuring: A smooth start

Too often we, me first, associate the concept to match but this is too restrictive. Let’s start with some let statements. Copy and paste the code below in the Rust Playground then hit CTRL+ ENTER

fn destructuring01() {
    println!("\nDestructuring 01 : 101\n");

    let (x, y) = (1, 2); // (x, y) is a pattern
    println!("{x}, {y}");

    let (x, y) = (1, 3.14); // tuple => we can have different data type
    println!("{x}, {y}");

    let [a, b, c] = [10, 20, 30]; // [a, b, c] is a pattern
    println!("{a}, {b}, {c}");

    let x = 42; // `x` is a very simple pattern: it matches any value and binds it to the name `x`
    println!("{x}");

    let ((x1, y1), (x2, y2)) = ((1, 2), (3, 4)); // nested destructuring
    println!("{x1}, {y1}, {x2}, {y2}");
}

fn main(){
    destructuring01();
}

Expected output

Destructuring 01 : 101

1, 2
10, 20, 30
42
1, 2, 3, 4

Explanations

  • As I said, destructuring is the act of using a pattern to break a value apart and extract its inner pieces. In this context, a pattern is a syntax that matches the shape of a value.
  • The first let statement matches (x, y) to (1, 2). Once this is OK shape wise, it extracts the value 1 and affect it to x and do the same with 2 and y. I told you. A smooth start.
  • The second let is similar to the first one except that x and y have different data type
  • The third let is similar to the first one but since we match arrays, a, b and c have the same data type
  • The fourth might be surprising. If the notion of binding is not crystal clear, you can read this
    page about Mutability
  • The last let statement shows nested destructuring, where like with Russian Dolls, match act at different levels.

Destructuring: Partial Destructuring

fn destructuring02() {
    println!("\nDestructuring 02 : partial destructuring\n");

    let (x, ..) = (1, 2, 3); // ignore the rest
    println!("x : {}", x);

    struct Point3D {
        x: i32,
        y: i32,
        z: i32,
    }
    let pt = Point3D { x: 1, y: 2, z: 3 };

    let Point3D { x, .. } = pt;
    println!("x coordinates: {}", x);
}



Expected output

Explanations


Destructuring: struct with let

fn destructuring03() {
    println!("\nDestructuring 03 : a struct with let\n");

    struct Scientist {
        name: String,
        field: String,
    }

    let hari = Scientist {
        name: "Hari Seldon".to_string(),
        field: "Psychohistory".to_string(),
    };

    let Scientist { name, field } = hari;
    println!("{name} works in {field}");
}


Expected output

Explanations


Destructuring: enum with let

fn destructuring04() {
    println!("\nDestructuring 04 : enum with let\n");

    enum Role {
        Emperor,
        Trader(String),
        Scientist { name: String, field: String },
    }

    let characters = vec![
        Role::Emperor,
        Role::Trader("Hober Mallow".to_string()),
        Role::Scientist {
            name: "Hari Seldon".to_string(),
            field: "Psychohistory".to_string(),
        },
    ];

    for role in characters {
        match role {
            Role::Emperor => println!("The Emperor rules... vaguely."),
            Role::Trader(name) => println!("A trader named {name}"),
            Role::Scientist { name, field } => {
                println!("Scientist {name} specializes in {field}")
            }
        }
    }
}


Expected output

Explanations


Destructuring: tuples with let 1/2

fn destructuring05() {
    println!("\nDestructuring 05 : tuples with let 1/2\n");

    let (name, age) = ("Salvor Hardin", 42); // tuple destructuring
    let Some(x) = Some(5) else { todo!() }; // enum destructuring

    fn print_coords((x, y): (i32, i32)) {
        println!("{x}, {y}");
    }

    let (my_x, my_y) = (28, 56);
    print_coords((my_x, my_y));
}


Expected output

Explanations


Destructuring: tuples with let 2/2

// When destructuring, the pattern on the left-hand side must match the shape of the value on the right.
// In this case, a 2-element tuple is matched by a 2-element pattern.
fn destructuring06() {
    println!("\nDestructuring 06 : tuples with let 2/2\n");

    let pair = ("Hari Seldon", 12050);

    // Destructuring the tuple into two separate variables
    let (name, year) = pair;

    println!("{} was born in year {}", name, year);

    // You can also ignore parts of a tuple using _
    let (_, just_the_year) = pair;
    println!("We only care about the year: {}", just_the_year);
}


Expected output

Explanations


Destructuring: function & closure parameters


fn destructuring07() {
    println!("\nDestructuring 07 : function & closure parameters\n");

    // --- Function with destructured parameters ---
    fn print_coordinates((x, y): (i32, i32)) {
        println!("Function received: x = {}, y = {}", x, y);
    }

    let point = (10, 20);
    print_coordinates(point);

    // --- Destructuring in a let binding ---
    let (a, b) = point;
    println!("Destructured binding: a = {}, b = {}", a, b);

    // --- Destructuring in a closure ---
    let points = vec![(1, 2), (3, 4), (5, 6)];

    println!("\nClosure with destructuring:");
    points.iter().for_each(|&(x, y)| {
        println!("Point: x = {}, y = {}", x, y);
    });
}

Expected output

Destructuring 01 : function parameters

x = 10, y = 20
a = 10, b = 20

Explanations


Destructuring: in for loops with .enumerate()

// In a for loop, the variable immediately after for is a pattern.
// That’s why we can destructure tuples directly inside the loop.”
fn destructuring08() {
    println!("\nDestructuring 08 : in for loops with enumerate()\n");

    let characters = vec!["Hari", "Salvor", "Hober"];

    for (index, name) in characters.iter().enumerate() {
        // (index, name) a pattern that destructures the (usize, &str) tuple
        println!("Character #{index} is {name}");
    }

    // Underscore can be used to ignore parts
    for (_, name) in characters.iter().enumerate() {
        println!("Name only: {name}");
    }
}


Expected output

Explanations


Destructuring: for loop over array slices

// This line might look like we're referencing s, but &[x, y] is a pattern, not a reference. The compiler matches each &[i32; 2] and destructures it in-place
fn destructuring09() {
    println!("\nDestructuring 09 : for loop over array slices\n");

    let coordinates = vec![[1, 2], [3, 4], [5, 6]];

    // Each element is a reference to an array: &[i32; 2]
    // Destructuring pattern applied to &[i32; 2]
    for &[x, y] in &coordinates {
        // &[x, y] pattern that matches a reference to a 2-element array
        println!("x: {}, y: {}", x, y);
    }

    // Alternative: without destructuring
    for coord in &coordinates {
        println!("coord[0]: {}, coord[1]: {}", coord[0], coord[1]);
    }
}


Expected output

Explanations


Destructuring: destructuring pattern in for loop

This is the part that broke my brain when I first encountered it.

When iterating over a vector of strings by reference (&Vec), I naively thought that writing for &s in &foundation meant “give me the reference and then give me the value.” But that’s not what’s happening.

In Rust, the expression after for is always a pattern — and here, &s is a destructuring pattern, not a reference.

It tries to match a &String (which is what &foundation yields) with the pattern &s, which would only work if s were a String. But in Rust, you can’t implicitly copy or clone a String, so it fails to compile.

Lesson learned: in a for loop, if you write &s, you’re telling the compiler: “I want to destructure a reference and bind the value inside it.” It’s not the same as taking a reference.

fn destructuring10() {
    println!("\nDereferencing 10 : destructuring pattern in for loop\n");

    let foundation: Vec<String> = vec!["Hari Seldon", "Salvor Hardin", "Hober Mallow", "The Mule", "Bayta Darell"]
        .into_iter()
        .map(|s| s.to_string())
        .collect();

    // The following loop will not compile
    // In a for loop, the value that directly follows the keyword for is a pattern
    // So `s`is NOT variable, &s is not a reference, &s is a pattern - specifically, a destructuring pattern.

    // for &s in &foundation {
    //     println!("String is : {}", s);
    // }

    for s in &foundation {
        println!("String is : {}", s);
    }
}

Expected output



Explanations


Destructuring: Option in a for loop

// Patterns can be used in loops to filter and destructure in a single step. Here, &Some(score) is not a reference — it’s a pattern that matches a reference to an Option and destructures it if it’s Some
fn destructuring11() {
    println!("\nDestructuring 11 : Option<T> in a for loop\n");

    let maybe_scores = vec![Some(10), None, Some(30)];

    // The pattern is a reference to an Option, so we match &Some(x)
    for &opt in &maybe_scores {
        match opt {
            Some(score) => println!("Score: {}", score),
            None => println!("No score"),
        }
    }

    // Alternative: filter out None before the loop
    for score in maybe_scores.iter().filter_map(|opt| opt.as_ref()) {
        println!("Got a score (filter_map): {}", score);
    }

    // Using if-let inside the loop body
    // Using if-let inside the loop body
    for maybe in &maybe_scores {
        if let Some(score) = maybe {
            println!("Score via if-let: {}", score);
        }
    }

    // Rather than going through a Vec<Option<T>>, and ignoring the None in the loop, we can avoid the if let by flattening the Some directly in the iterator.
    for score in maybe_scores.iter().flatten() {
        println!("Score via flatten: {}", score);
    }
}


Expected output

Explanations

Conclusion

Aspect Dereferencing Destructuring
Syntax *x let (a, b) = x
Semantics Access pointed value Extract elements of a structure
Applicable to &T, Box<T>, etc. tuple, struct, enum, array, etc.
Requires traits? Yes: Deref No (structural pattern matching)
  • Summarizes the distinction.
    • Dereferencing is peeling a wrapper off, destructuring is breaking the thing into pieces.
    • The word pattern refers to the left-hand side of an assignment, match or for.
    • Destructuring occurs as soon as you “break a structure into pieces”, thanks to this pattern.
  • Encourages experimentation (with for, let, if let, while let, function parameters).
  • Bonus: suggest a toy implementation to play with Deref and Drop to see the effects.

  • Thinking that destructuring is only possible in match
  • Incorrect understanding of pattern in for

Webliography


Back to top

Published on: Jun 27 2025 at 09:00 AM | Last updated: Jun 27 2025 at 09:00 AM

Copyright © 1964-2025 - 40tude