August 10, 20255 min

mutable vs. immutable borrows.

m
mayo

Rust’s borrowing rules, enforced by the borrow checker at compile time, ensure memory safety and prevent data races without runtime overhead. These rules govern how data can be accessed via references, distinguishing between mutable (&mut T) and immutable (&T) borrows.

OK: Many &T &x, &x, &x, ... read-only, unlimited count OK: One &mut T &mut x exclusive access, no others Rejected: Mixed &x + &mut x compile error: data race risk Borrow checker enforces this at compile time no runtime cost, no data races possible in safe code

The Borrowing Rules (Compiler-Enforced)

  1. Either One Mutable Borrow (&mut T) OR Multiple Immutable Borrows (&T):
    • You can have:
      • One mutable reference (&mut T), OR
      • Any number of immutable references (&T).
    • Never both at the same time for the same data.
  2. References Must Always Be Valid (No Dangling Pointers):
    • Borrowed references cannot outlive the data they point to, enforced by Rust’s lifetime system.

Immutable Borrows (&T)

  • Read-only access: Cannot modify the data.
  • Multiple allowed: Safe for concurrent reads, as no modifications can occur.

Example:

let x = 42;
let r1 = &x;  // OK: Immutable borrow
let r2 = &x;  // OK: Another immutable borrow
println!("{}, {}", r1, r2);  // Works fine

Mutable Borrows (&mut T)

  • Exclusive access: Allows modification of the data.
  • No other borrows allowed: No &T or additional &mut T can coexist for the same data.

Example:

let mut x = 42;
let r1 = &mut x;  // OK: Mutable borrow
*r1 += 1;         // Can modify
// let r2 = &x;   // ERROR: Cannot borrow `x` as immutable while mutable borrow exists

Compiler Rejects These Scenarios

  1. Mutable + Immutable Overlap:

    let mut data = 10;
    let r1 = &data;      // Immutable borrow
    let r2 = &mut data;  // ERROR: Cannot borrow as mutable while borrowed as immutable
    
  2. Multiple Mutable Borrows:

    let mut s = String::new();
    let r1 = &mut s;
    let r2 = &mut s;  // ERROR: Second mutable borrow
    
  3. Dangling References:

    fn dangling() -> &String {
        let s = String::from("oops");
        &s  // ERROR: `s` dies here, reference would dangle
    }
    

What the checker actually compares is not whether both borrows exist, but whether their live spans touch. A borrow's span ends at its last use, not at the end of the block:

Rejected: the shared borrow is still live later 1. let r1 = &data; 2. let r2 = &mut data; 3. println!("{}", r1); &data live until its last use on line 3 &mut data live from line 2 onward both live at once, so rustc refuses Accepted: same two borrows, reordered 1. let r1 = &data; 2. println!("{}", r1); 3. let r2 = &mut data; &data span closes at last use &mut data now exclusive No instant has both spans live, so the same statements now compile. time

Why These Rules Matter

  • Prevents Data Races: By disallowing concurrent mutable access, Rust ensures thread safety by default.
  • Ensures Memory Safety: No dangling pointers or iterator invalidation, as the borrow checker enforces valid references.

Key Takeaways

Immutable borrows (&T):

  • Many allowed, but no mutation. ✅ Mutable borrows (&mut T):
  • Only one allowed, exclusive access. 🚫 Violations caught at compile time: No runtime overhead.

Real-World Impact: These rules enable fearless concurrency, as seen in crates like Rayon for parallel iteration.

Experiment: Try creating a function that takes &mut T and call it twice with the same data.
Answer: The borrow checker won’t allow it unless the first borrow’s scope ends, preventing overlapping mutable borrows.