Interior Mutability

Interior Mutability vs. Mutable References

Choosing between compile-time mutable references (&mut T) and interior mutability (&Cell<T>, &RefCell<T>, &Mutex<T>) is a core API design decision in Rust. Mutable references make state transitions explicit in function signatures and verify aliasing at compile time with zero runtime cost, whereas interior mutability encapsulates mutation behind &self when sharing or trait contracts require it.

Side-by-Side Comparison

Property Mutable Reference (&mut T) Interior Mutability (Cell, RefCell, Mutex)
API transparency Explicit: signature fn update(&mut self) signals mutation to callers Hidden: signature fn update(&self) looks read-only from the outside
Compile-time borrow checking Yes (E0502 / E0499 caught during cargo check) No (RefCell panics at runtime; Mutex can deadlock)
Compiler optimizations Enables LLVM noalias register caching and loop hoisting Disables readonly assumptions inside UnsafeCell<T>
Works with Rc<T> / Arc<T>? Only when strong_count == 1 (get_mut / make_mut) Yes, with arbitrary active owners
Granular struct borrowing Borrow checker automatically tracks disjoint struct fields (&mut s.a, &s.b) Requires wrapping individual fields or splitting guards

Splitting Struct Borrows Instead of Reaching for RefCell<T>

A common anti-pattern is wrapping a struct field in RefCell<T> solely because calling a helper method self.helper() while holding a borrow of self.other_field triggers E0502 (cannot borrow *self as mutable because it is also borrowed as immutable).

Because a method taking &mut self borrows every field of self in its signature, factoring the helper to accept only the specific fields it needs—or borrowing disjoint fields directly—keeps everything checked at compile time without RefCell:

struct Engine {
    values: Vec<i32>,
    audit_log: Vec<String>,
}

impl Engine {
    // Associated helper taking only `&mut Vec<String>` instead of `&mut self`:
    fn record_log(log: &mut Vec<String>, msg: String) {
        log.push(msg);
    }

    fn process_all(&mut self) {
        // The borrow checker sees that `&self.values` and `&mut self.audit_log`
        // are completely disjoint fields of `self`:
        for &val in &self.values {
            Self::record_log(&mut self.audit_log, format!("processed {}", val));
        }
    }
}

fn main() {
    let mut engine = Engine {
        values: vec![10, 20, 30],
        audit_log: Vec::new(),
    };
    engine.process_all();
    println!("audit_log: {:?}", engine.audit_log);
}
audit_log: ["processed 10", "processed 20", "processed 30"]

When to Prefer Each Approach

  1. Prefer &mut T (Inherited Mutability) whenever:
    • The caller logically owns or exclusively controls the value during the mutation.
    • Mutating the value changes its observable domain state (such as pushing to a collection or advancing an iterator).
    • Disjoint field borrowing or slice splitting (slice.split_at_mut(mid)) can express the simultaneous borrows statically.
  2. Prefer Interior Mutability whenever:
    • Multiple owners (Rc<T> / Arc<T>) must mutate shared state.
    • An external trait mandates &self (for example, Clone, Display, or an async/RPC handler trait).
    • The mutation is an invisible implementation detail of a logically immutable operation (such as populating a OnceCell<T> / OnceLock<T> cache or incrementing a diagnostic counter).