Rust's ownership system is built on three rules: each value has one owner, ownership can be transferred, and you can borrow references to values. References come in two flavors: immutable (&) and mutable (&mut).
In method signatures, &mut self means the method can modify the instance:
struct Counter {
count: u32,
}
impl Counter {
fn increment(&mut self) {
self.count += 1;
}
fn value(&self) -> u32 {
self.count
}
}
let mut counter = Counter { count: 0 };
counter.increment();
println!("{}", counter.value()); // 1
The increment method takes &mut self because it modifies count. The value method takes &self because it only reads.
Borrowing Rules
Rust enforces strict borrowing rules at compile time:
- You can have multiple immutable references (
&T) to a value at the same time - You can have exactly one mutable reference (
&mut T) to a value - You can't have both mutable and immutable references to the same value simultaneously
These rules prevent data races. If one part of the code is modifying data, no other part can read or modify it at the same time.
let mut x = 5;
let r1 = &x; // OK
let r2 = &x; // OK - multiple immutable borrows allowed
let r3 = &mut x; // Error: can't borrow as mutable while immutable borrows exist
The compiler rejects this because r1 and r2 assume x won't change. If r3 modifies x, those assumptions break.
Method Receiver Types
Methods can take self in three ways:
impl MyType {
fn consume(self) { } // Takes ownership
fn borrow(&self) { } // Immutable borrow
fn mutate(&mut self) { } // Mutable borrow
}
self (no &) consumes the value. The caller can't use it afterward. This is rare—usually for converting the type into something else.
&self borrows immutably. The method can read but not modify. This is the most common pattern for getter methods and operations that don't change state.
&mut self borrows mutably. The method can read and modify. Use this for setters and operations that change the instance.
Why &mut self?
The explicit &mut makes mutation visible in the method signature. When you call a method, you know whether it might modify the value:
counter.value(); // Reads - takes &self
counter.increment(); // Modifies - takes &mut self
In languages without this distinction, any method call might mutate. In Rust, the signature tells you.
This clarity helps when reasoning about code. If a function takes &T, you know it won't modify the value. If it takes &mut T, modification is possible.
Mutable Variable Requirement
To call a method that takes &mut self, the variable must be declared mutable:
let counter = Counter { count: 0 };
counter.increment(); // Error: counter is not mutable
let mut counter = Counter { count: 0 };
counter.increment(); // OK
The mut keyword on the binding signals that the value can be modified. This is separate from the reference type—let mut makes the binding mutable, &mut creates a mutable reference.
Lifetime of Borrows
Borrows have a lifetime—the scope where the reference is valid. The compiler ensures references don't outlive the data they point to:
let mut x = 5;
{
let r = &mut x;
*r += 1;
} // r goes out of scope here
println!("{}", x); // OK - no active mutable borrow
The mutable borrow r ends when its scope ends. After that, you can use x normally.
Dereferencing with *
To modify the value behind a mutable reference, use the dereference operator *:
let mut x = 5;
let r = &mut x;
*r += 1; // Modify the value x points to
In method bodies, self is automatically dereferenced when accessing fields:
fn increment(&mut self) {
self.count += 1; // Automatically dereferenced
// Equivalent to: (*self).count += 1;
}
Rust's automatic dereferencing makes code cleaner without sacrificing control.
Interior Mutability
Sometimes you need to mutate data through an immutable reference. This seems to violate the borrowing rules, but Rust provides types that allow controlled mutation:
use std::cell::RefCell;
struct Cache {
data: RefCell<HashMap<String, String>>,
}
impl Cache {
fn get(&self, key: &str) -> Option<String> {
// Mutate through &self using RefCell
self.data.borrow_mut().insert(key.to_string(), "value".to_string());
self.data.borrow().get(key).cloned()
}
}
RefCell provides runtime-checked borrowing. It enforces the borrowing rules at runtime instead of compile time. If you violate them, the program panics.
This is useful for caching, reference counting with mutation, and other patterns where compile-time checks are too strict.
Shared Mutable State with Mutex
For thread-safe mutation, use Mutex:
use std::sync::Mutex;
let counter = Mutex::new(0);
{
let mut guard = counter.lock().unwrap();
*guard += 1;
} // Lock released here
The mutex ensures only one thread can access the value at a time. The MutexGuard acts like a mutable reference with automatic lock release when dropped.
Common Mistakes
Trying to borrow mutably while immutable borrows exist:
let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(4); // Error: can't borrow mutably while immutable borrow exists
println!("{}", first);
The fix is to limit the lifetime of first:
let mut v = vec![1, 2, 3];
{
let first = &v[0];
println!("{}", first);
} // first goes out of scope
v.push(4); // OK
Forgetting to declare variables mutable:
let x = 5;
let r = &mut x; // Error: x is not mutable
Add mut to the binding:
let mut x = 5;
let r = &mut x; // OK
When to Use &mut self
Use &mut self when:
- The method modifies the instance's state
- You're implementing builder patterns where methods chain and modify
- You need exclusive access to prevent concurrent modification
Use &self when:
- The method only reads data
- Multiple calls can safely happen concurrently
Use self when:
- The method consumes the value (conversions, final cleanup)
The Safety Guarantee
Rust's borrow checker prevents:
- Use-after-free (references can't outlive the data)
- Data races (no concurrent mutable access)
- Iterator invalidation (can't modify a collection while iterating)
These are compile-time guarantees. If your code compiles, these classes of bugs don't exist.
The tradeoff is fighting the borrow checker initially. The rules feel restrictive until you internalize them. Then they become natural, and you wonder how you wrote safe code without them.
Further Reading
The Rust Book's chapter on references and borrowing is the canonical introduction.
For deeper understanding, see the Rustonomicon's coverage of ownership and lifetimes.
Jon Gjengset's Crust of Rust: Lifetime Annotations video explains lifetimes and borrowing in detail.
Understanding &mut self and mutable references is central to writing safe, concurrent Rust code.
0 comments