| |

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.

CombinationAllowed
Many &TYes
One &mut TYes
&T and &mut T togetherNo
Two &mut TNo

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.

SignatureMeaningCaller requirement
fn f(s: &T)Reads onlyImmutable or mutable binding
fn f(s: &mut T)Reads and writesMutable binding
fn f(s: T)Takes ownershipAny 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

KindSyntaxReadWriteCount
Immutable&TYesNoMany
Mutable&mut TYesYesOne

The Rule

CombinationAllowed
Many &TYes
One &mut TYes
&T + &mut TNo
Two &mut TNo

Function Signatures

SignatureMeaningCaller binding
fn f(s: &T)ReadsAny
fn f(s: &mut T)Reads and writesMutable
fn f(s: T)Takes ownershipAny

Idiomatic Types

Instead ofUseReason
&String&strMore general
&Vec<T>&[T]More general
&Box<T>&TDeref coercion

Common Patterns

PatternSyntax
Multiple readerslet r1 = &s; let r2 = &s;
One writerlet w = &mut s;
Reborrowmodify(r); modify(r);
Explicit reborrow&mut *r

Borrow Checker Errors

ErrorCause
cannot borrow as mutableImmutable borrow still live
cannot borrow as immutableMutable borrow still live
cannot borrow as mutable more than onceTwo mutable borrows
does not live long enoughReference 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

PitfallWhy It HappensFix
Cannot borrow as mutableImmutable borrow still liveLet the immutable borrow end first
Cannot borrow as immutableMutable borrow still liveLet the mutable borrow end first
Two mutable borrowsOverlapping scopesSeparate the scopes
Dangling referenceReturning a reference to a localReturn an owned value
Iterator invalidationMutating during iterationCollect first
&String instead of &strLess generalUse &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

ItemValue
Immutable reference&T
Mutable reference&mut T
Immutable countMany allowed
Mutable countOne at a time
RuleAliasing XOR mutation
Reborrowmodify(r) reborrows
Non-lexical lifetimesBorrow ends at last use
Iterator invalidationRejected at compile time
Dangling referenceRejected 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 &T allows reading and can coexist with other immutable references. A mutable reference &mut T allows 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 &T and &mut T is a design decision. A read-only function takes &T; a modifying function takes &mut T. The signature documents the intent.
  • Prefer &str over &String and &[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!