This post is still being written.
Rust Dereferencing vs Destructuring — For the Kids 2/2

Thanks Chat GPT
TL;DR
- Dereferencing: accessing the value behind a reference or smart pointer (e.g.,
*x
, or implicit viaDeref
). 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.
The Post is in 2 Parts
- The introduction is the same
- Rust Dereferencing vs Destructuring — For the Kids 1/2
- Rust Dereferencing vs Destructuring — For the Kids 2/2
Table of Contents
- Introduction
- Patterns and Destructuring Patterns in Rust
- Destructuring: A smooth start
- Destructuring: Partial Destructuring
- Destructuring:
struct
withlet
- Destructuring:
enum
with let - Destructuring:
tuples
withlet
1/2 - Destructuring:
tuples
withlet
2/2 - Destructuring: function & closure parameters
- Destructuring: in
for
loops with.enumerate()
- Destructuring:
for
loop over array slices - Destructuring: destructuring pattern in for loop
- Destructuring: Option
in a for loop - Rust Gotchas: Destructuring Edition
- Conclusion
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 Post in 2 Parts Covers
- Dereferencing: Part 1/2. How to access values through references and smart pointers (Box, Rc, RefCell), and how mutability affects this.
- Destructuring: Part 2/2. 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.
Now that we’ve seen in Part 1 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 tox
and do the same with 2 andy
. I told you. A smooth start.- I hope why
let
is a statement and not an expression. If not, read this Computer Science Vocabulary page the this one.
- I hope why
- The second
let
is similar to the first one except thatx
andy
have different data type - The third
let
is similar to the first one but since we match arrays,a
,b
andc
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
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
Any tips and tricks to share ? Here are a few common traps and surprises you might encounter (I did)
Rust Gotchas: Destructuring Edition
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
andDrop
to see the effects. - Thinking that destructuring is only possible in
match
- Incorrect understanding of pattern in
for
Webliography
- Patterns in the Rust Reference
- let statement in the Rust Reference