Rust 16 🦀 Mutable References vs Immutable References
Rust has two kinds of references: immutable (&T) and mutable (&mut T). The distinction is enforced by the borrow checker, and it is the mechanism that prevents data races and aliasing bugs at compile time. An immutable reference allows reading the value but not modifying it, and any number of them can exist at the same time. A mutable reference allows reading and modifying, but only one can exist at a time, and while it exists, no other reference to the same value may exist. This rule — many readers or one writer, but never both — is the core of Rust’s memory safety guarantee.
The rule is often summarized as “aliasing XOR mutation.” If you have aliasing (multiple references), there is no mutation. If you have mutation (one reference), there is no aliasing. This is what makes Rust code free of data races: a data race requires two references to the same memory, at least one of which writes. The borrow checker forbids the combination, so data races cannot occur in safe Rust.
This chapter covers immutable references, mutable references, the exclusivity rule, the aliasing rule, reborrowing, the interaction with function signatures, the borrow checker’s error messages, and the patterns that satisfy the rules.
Key point: An immutable reference &T allows reading and can coexist with other immutable references. A mutable reference &mut T allows reading and writing but is exclusive: while it exists, no other reference — mutable or immutable — may exist to the same value. This is “aliasing XOR mutation.” The borrow checker enforces the rule at compile time, and the result is freedom from data races in safe Rust.
Why the two reference kinds exist
The data-race problem. A data race occurs when two threads access the same memory concurrently and at least one of them writes. In languages without compile-time enforcement, this produces undefined behavior. Rust’s rule — one mutable reference or many immutable references — makes the combination impossible, so data races cannot occur in safe code.
The aliasing problem. When two pointers refer to the same memory and one writes, the other sees a value it did not expect. Iterator invalidation is a classic example: a loop that holds a reference to a collection while the collection is modified. Rust’s borrow checker rejects the pattern, so the bug cannot be written.
The optimization problem. The compiler can optimize based on the guarantee that an immutable reference’s value does not change. It can cache the value in a register, reorder reads, and eliminate redundant loads. A mutable reference, by contrast, allows the compiler to assume no other reference observes the value, which enables other optimizations.
The reasoning problem. The two kinds of references document intent. A function that takes &T does not modify its argument. A function that takes &mut T may modify it, and the caller knows that the value is exclusively borrowed for the duration of the call. The signatures are self-documenting.
The API-design problem. The choice between the two is a design decision. A function that only reads should take &T, because it accepts more inputs and does not require the caller to have a mutable binding. A function that modifies should take &mut T, and the caller must have a mutable binding to pass one.
a. Immutable references
An immutable reference is written &T. It allows reading the value but not modifying it.
fn length(s: &String) -> usize {
s.len()
}
fn main() {
let s = String::from("hello");
let n = length(&s);
println!("{} has {} characters", s, n);
}
The function takes &String, reads its length, and returns. The s in main is still valid after the call, because the borrow ends when the function returns.
Many immutable references can coexist:
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &s;
println!("{} {} {}", r1, r2, r3);
All three references read the same value. The borrow checker permits this because none of them can modify the value, so there is no risk of one seeing a change made by another.
An immutable reference can be copied freely, because a reference is Copy:
let r1 = &s;
let r2 = r1; // copies the reference, both are valid
The reference is a pointer, and copying a pointer does not affect the value it points to.
b. Mutable references
A mutable reference is written &mut T. It allows reading and modifying the value.
fn add_exclamation(s: &mut String) {
s.push_str("!");
}
fn main() {
let mut s = String::from("hello");
add_exclamation(&mut s);
println!("{}", s); // hello!
}
The variable must be declared mut to allow a mutable reference. The function takes &mut String, modifies it, and returns. The s in main is still valid after the call.
A mutable reference is exclusive. While it exists, no other reference to the same value may exist:
let mut s = String::from("hello");
let r = &mut s;
r.push_str("!");
// let r2 = &s; // Error: cannot borrow s as immutable
// let r3 = &mut s; // Error: cannot borrow s as mutable more than once
The exclusivity is what prevents aliasing. If two mutable references existed, one could invalidate the other’s view of the data. If a mutable and an immutable reference existed, the immutable reference could read a value that was being changed.
A mutable reference can be reborrowed, which creates a temporary mutable reference that is passed to a function. The original reference is restored after the call:
fn modify(s: &mut String) {
s.push_str("!");
}
let mut s = String::from("hello");
let r = &mut s;
modify(r); // reborrows r
modify(r); // still valid
The reborrow is what allows chained mutable method calls:
let mut v = vec![1, 2, 3];
v.push(4);
v.push(5);
Each push takes a mutable reference to v, and the borrow ends when the call returns. The next push creates a new borrow.
c. The exclusivity rule
The rule is: at any given time, a value may have either one mutable reference or any number of immutable references, but not both.
| Combination | Allowed |
|---|---|
Many &T | Yes |
One &mut T | Yes |
&T and &mut T together | No |
Two &mut T | No |
The rule is checked at compile time. The borrow checker tracks the lifetimes of every reference and rejects code that violates it.
let mut data = vec![1, 2, 3];
// Multiple readers — OK
let r1 = &data;
let r2 = &data;
println!("{:?} {:?}", r1, r2);
// One writer — OK (after the readers are last used)
let w = &mut data;
w.push(4);
The readers and the writer do not overlap. The readers are last used in the println!, and the writer takes exclusive access afterward.
The borrow checker uses non-lexical lifetimes, which means a borrow ends at its last use, not at the end of the enclosing block. This makes many patterns work that would have been rejected by a purely lexical analysis.
d. The aliasing rule in practice
The aliasing rule is the reason several common patterns in other languages are rejected in Rust.
Iterator invalidation.
let mut v = vec![1, 2, 3];
// Error: cannot borrow v as mutable because it is also borrowed as immutable
for item in &v {
v.push(*item);
}
The for loop holds an immutable borrow of v for the duration of the iteration, and the v.push inside the loop tries to borrow it mutably. The compiler rejects this.
The fix is to iterate over a clone or to collect the changes and apply them after:
let v = vec![1, 2, 3];
let doubled: Vec<i32> = v.iter().map(|n| n * 2).collect();
The iter() borrows immutably, and the map produces a new collection. The conflict does not arise.
Holding a reference while modifying.
let mut data = vec![1, 2, 3];
let first = &data[0];
// data.push(4); // Error if first is used after
println!("{}", first);
With non-lexical lifetimes, the borrow ends after the println!, so the push is allowed if it comes after. If the push comes before the println!, the borrow is still live and the code is rejected.
Returning a reference to a local.
// Error: returns a reference to a local value
fn dangling() -> &String {
let s = String::from("hello");
&s
}
The String is dropped at the end of the function, so any reference to it would dangle. The compiler rejects the code.
e. Reborrowing
Reborrowing is the mechanism that allows a mutable reference to be passed to a function without being consumed. When a &mut T is passed to a function that takes &mut T, Rust implicitly reborrows it, creating a temporary mutable reference that is used for the call. The original reference is available after the call.
fn modify(s: &mut String) {
s.push_str("!");
}
let mut s = String::from("hello");
let r = &mut s;
modify(r); // reborrows r
modify(r); // r is still valid
println!("{}", s); // hello!!
The reborrow is what makes method chains work:
let mut v = vec![1, 2, 3];
v.push(4);
v.push(5);
v.push(6);
Each method call takes a mutable reference, and the borrow ends when the call returns. The next call creates a new borrow.
Reborrowing can be made explicit with the &mut *r syntax, but it is rarely necessary. The compiler inserts the reborrow automatically in the common cases.
let r2 = &mut *r; // explicit reborrow
The explicit form is useful when the automatic reborrow does not happen, such as when passing a reference through a generic function.
f. References in function signatures
The choice between &T and &mut T in a function signature is a design decision.
| Signature | Meaning | Caller requirement |
|---|---|---|
fn f(s: &T) | Reads only | Immutable or mutable binding |
fn f(s: &mut T) | Reads and writes | Mutable binding |
fn f(s: T) | Takes ownership | Any binding; value is moved |
A function that only reads should take &T, because it accepts more inputs. A caller with an immutable binding can pass &T, and a caller with a mutable binding can also pass &T (the mutable binding is borrowed immutably for the call).
A function that modifies should take &mut T. The caller must have a mutable binding, and the value is exclusively borrowed for the duration of the call.
fn read(s: &str) -> usize {
s.len()
}
fn modify(s: &mut String) {
s.push_str("!");
}
fn consume(s: String) {
// takes ownership
}
The idiomatic choice for a string parameter is &str over &String, and for a slice parameter is &[T] over &Vec<T>. The reference types are more general and accept more inputs.
fn length(s: &str) -> usize { s.len() }
let s = String::from("hello");
length(&s); // &String coerces to &str
length("hello"); // &str literal
The same principle applies to slices: &[T] accepts both arrays and vectors.
Complete Example Session
// ============================================
// PART 1: IMMUTABLE REFERENCE
// ============================================
fn length(s: &String) -> usize {
s.len()
}
fn main() {
let s = String::from("hello");
let n = length(&s);
println!("{} has {} characters", s, n);
}
// ============================================
// PART 2: MULTIPLE IMMUTABLE REFERENCES
// ============================================
fn main() {
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &s;
println!("{} {} {}", r1, r2, r3);
}
// ============================================
// PART 3: MUTABLE REFERENCE
// ============================================
fn add_exclamation(s: &mut String) {
s.push_str("!");
}
fn main() {
let mut s = String::from("hello");
add_exclamation(&mut s);
println!("{}", s); // hello!
}
// ============================================
// PART 4: EXCLUSIVITY
// ============================================
fn main() {
let mut s = String::from("hello");
let r = &mut s;
r.push_str("!");
// let r2 = &s; // Error
// let r3 = &mut s; // Error
}
// ============================================
// PART 5: NON-LEXICAL LIFETIMES
// ============================================
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2);
let r3 = &mut s;
r3.push_str("!");
println!("{}", r3);
}
// ============================================
// PART 6: BORROW ENDS AT LAST USE
// ============================================
fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0];
println!("{}", first);
v.push(4); // OK: first is no longer used
}
// ============================================
// PART 7: ITERATOR INVALIDATION REJECTED
// ============================================
// This does not compile:
//
// let mut v = vec![1, 2, 3];
// for item in &v {
// v.push(*item);
// }
// ============================================
// PART 8: FIX WITH COLLECT
// ============================================
fn main() {
let v = vec![1, 2, 3];
let doubled: Vec<i32> = v.iter().map(|n| n * 2).collect();
println!("{:?}", doubled);
println!("{:?}", v);
}
// ============================================
// PART 9: REBORROW
// ============================================
fn modify(s: &mut String) {
s.push_str("!");
}
fn main() {
let mut s = String::from("hello");
let r = &mut s;
modify(r);
modify(r);
println!("{}", s); // hello!!
}
// ============================================
// PART 10: SIGNATURE CHOICE
// ============================================
fn length(s: &str) -> usize { s.len() }
fn add_exclamation(s: &mut String) { s.push_str("!"); }
fn consume(s: String) { println!("{}", s); }
These ten parts cover immutable references, multiple immutable references, mutable references, exclusivity, non-lexical lifetimes, borrow ending at last use, iterator invalidation rejected, the fix with collect, reborrowing, and signature choice.
Quick Reference
Reference Kinds
| Kind | Syntax | Read | Write | Count |
|---|---|---|---|---|
| Immutable | &T | Yes | No | Many |
| Mutable | &mut T | Yes | Yes | One |
The Rule
| Combination | Allowed |
|---|---|
Many &T | Yes |
One &mut T | Yes |
&T + &mut T | No |
Two &mut T | No |
Function Signatures
| Signature | Meaning | Caller binding |
|---|---|---|
fn f(s: &T) | Reads | Any |
fn f(s: &mut T) | Reads and writes | Mutable |
fn f(s: T) | Takes ownership | Any |
Idiomatic Types
| Instead of | Use | Reason |
|---|---|---|
&String | &str | More general |
&Vec<T> | &[T] | More general |
&Box<T> | &T | Deref coercion |
Common Patterns
| Pattern | Syntax |
|---|---|
| Multiple readers | let r1 = &s; let r2 = &s; |
| One writer | let w = &mut s; |
| Reborrow | modify(r); modify(r); |
| Explicit reborrow | &mut *r |
Borrow Checker Errors
| Error | Cause |
|---|---|
cannot borrow as mutable | Immutable borrow still live |
cannot borrow as immutable | Mutable borrow still live |
cannot borrow as mutable more than once | Two mutable borrows |
does not live long enough | Reference outlives referent |
Best Practices
✅ Do This:
// Use &T for read-only functions
fn length(s: &str) -> usize { s.len() }
// Use &mut T for modifying functions
fn add(s: &mut String) { s.push_str("!"); }
// Let borrows end at their last use
let first = &v[0];
println!("{}", first);
v.push(4); // OK after first is last used
// Use &str over &String
fn greet(name: &str) -> String { format!("Hello, {}", name) }
// Reborrow mutable references
modify(&mut s);
modify(&mut s);
❌ Don’t Do This:
// Hold a mutable and immutable reference
let r1 = &s;
let r2 = &mut s; // ❌ if r1 is used later
// Mutate during iteration
for item in &v { v.push(*item); } // ❌
// Return a reference to a local
fn dangling() -> &String { &String::from("x") } // ❌
// Use &String when &str suffices
fn length(s: &String) -> usize { s.len() } // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Cannot borrow as mutable | Immutable borrow still live | Let the immutable borrow end first |
| Cannot borrow as immutable | Mutable borrow still live | Let the mutable borrow end first |
| Two mutable borrows | Overlapping scopes | Separate the scopes |
| Dangling reference | Returning a reference to a local | Return an owned value |
| Iterator invalidation | Mutating during iteration | Collect first |
&String instead of &str | Less general | Use &str |
Real-World Examples
1. Read-Only Function
fn length(s: &str) -> usize { s.len() }
2. Modifying Function
fn add_exclamation(s: &mut String) { s.push_str("!"); }
3. Multiple Readers
let r1 = &data;
let r2 = &data;
let r3 = &data;
4. One Writer
let w = &mut data;
w.push(1);
5. Borrow Ends
let first = &v[0];
println!("{}", first);
v.push(4);
6. Collect to Avoid Conflict
let doubled: Vec<_> = v.iter().map(|n| n * 2).collect();
7. Reborrow
modify(&mut s);
modify(&mut s);
8. Return Owned
fn create() -> String { String::from("new") }
9. Slice Parameter
fn sum(values: &[i32]) -> i32 {
values.iter().sum()
}
10. Mutable Iteration
for item in v.iter_mut() {
*item *= 2;
}
Visual
The Rule
┌──────────────────────────────────────────────────────────────┐
│ ALLOWED: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Many immutable references │ │
│ │ let r1 = &v; │ │
│ │ let r2 = &v; │ │
│ │ let r3 = &v; │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ One mutable reference │ │
│ │ let w = &mut v; │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ NOT ALLOWED: │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Immutable and mutable together │ │
│ │ let r = &v; │ │
│ │ let w = &mut v; // Error │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Two mutable references │ │
│ │ let w1 = &mut v; │ │
│ │ let w2 = &mut v; // Error │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Aliasing XOR Mutation
┌──────────────────────────────────────────────────────────────┐
│ ALIASING (many references) ──▶ no mutation │
│ MUTATION (one reference) ──▶ no aliasing │
│ │
│ The two cannot coexist. │
│ This is what makes data races impossible. │
└──────────────────────────────────────────────────────────────┘
Non-Lexical Lifetimes
┌──────────────────────────────────────────────────────────────┐
│ let mut s = String::from("hello"); │
│ │
│ let r1 = &s; ← borrow starts │
│ let r2 = &s; │
│ println!("{} {}", r1, r2); ← borrow ends here │
│ │
│ let r3 = &mut s; ← mutable borrow starts (OK) │
│ r3.push_str("!"); │
│ println!("{}", r3); ← mutable borrow ends │
└──────────────────────────────────────────────────────────────┘
Reborrow
┌──────────────────────────────────────────────────────────────┐
│ fn modify(s: &mut String) { s.push_str("!"); } │
│ │
│ let mut s = String::from("hello"); │
│ let r = &mut s; │
│ │
│ modify(r); ← reborrows r, r is still valid │
│ modify(r); ← reborrows r again │
│ │
│ println!("{}", s); // hello!! │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Immutable reference | &T |
| Mutable reference | &mut T |
| Immutable count | Many allowed |
| Mutable count | One at a time |
| Rule | Aliasing XOR mutation |
| Reborrow | modify(r) reborrows |
| Non-lexical lifetimes | Borrow ends at last use |
| Iterator invalidation | Rejected at compile time |
| Dangling reference | Rejected at compile time |
| Idiomatic string param | &str |
| Idiomatic slice param | &[T] |
Key takeaways:
- Rust has two kinds of references: immutable and mutable. An immutable reference
&Tallows reading and can coexist with other immutable references. A mutable reference&mut Tallows reading and writing but is exclusive. - The rule is “aliasing XOR mutation.” At any given time, a value may have either one mutable reference or many immutable references, but not both. This is checked at compile time.
- The rule prevents data races. A data race requires two references to the same memory, at least one of which writes. The borrow checker forbids the combination, so data races cannot occur in safe Rust.
- Non-lexical lifetimes end a borrow at its last use. A reference is no longer considered active after the last point where it is used, which makes many patterns work that a purely lexical analysis would reject.
- Reborrowing allows a mutable reference to be passed to a function without being consumed. The compiler inserts the reborrow automatically, and the original reference is available after the call.
- The choice between
&Tand&mut Tis a design decision. A read-only function takes&T; a modifying function takes&mut T. The signature documents the intent. - Prefer
&strover&Stringand&[T]over&Vec<T>. The reference types are more general and accept more inputs through deref coercion.
Remember: The distinction between mutable and immutable references is the heart of Rust’s borrow checker. The rule — many readers or one writer, never both — is what makes Rust code free from data races and aliasing bugs. The rule is enforced at compile time, so the safety costs nothing at runtime. Non-lexical lifetimes make the rule ergonomic by ending borrows at their last use, and reborrowing makes mutable references composable. When the borrow checker rejects code, the fix is usually to shorten the lifetime of a borrow, to use an immutable reference instead of a mutable one, or to restructure so that the conflicting borrows do not overlap. Understanding the two kinds of references is understanding how Rust’s safety guarantees work.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!