On this page

Cell, RefCell, Mutex: Interior Mutability

Rust’s shared references (&T) are immutable by default—Cell/RefCell/Mutex provide varying degrees of interior mutability under the premise of "sharing". Cell is suitable for Copy types (no runtime overhead), RefCell defers borrow checking to runtime (panic on error), and Mutex adds thread-safe locking (Send).

Origin of the Problem

let x = 5;
let r = &x;                          // &i32, immutable
// *r = 6;                           // ERROR: &T cannot be modified

Rust’s core rule: data cannot be modified through shared references (&T). This prevents data races and iterator invalidation. However, sometimes logically safe modifications are rejected by the rules—such as caching computed results or internal reference counting. Interior mutability allows you to modify internal state while holding &self.

Cell: Replacing the Entire Value (Copy Types Only)

use std::cell::Cell;
let c = Cell::new(42);
c.set(100);                          // Replace internal value — no &mut self needed
let val = c.get();                   // Copy out

Cell does not return internal references—get() always copies the value (requires T: Copy), and set() always replaces the entire value. No references leak to the outside, so the borrow checker does not block it. Suitable for simple wrappers of i32, bool, enum, etc.

RefCell: Runtime Borrow Check

use std::cell::RefCell;
let rc = RefCell::new(vec![1, 2, 3]);
{
    let mut borrow = rc.borrow_mut(); // runtime: check for no shared borrow
    borrow.push(4);
}                                    // borrow_mut guard dropped → borrow released
let borrow = rc.borrow();            // runtime: OK

The rules are the same as the compile-time borrow checker—aliasing XOR mutation—but the check occurs at runtime. Violation → panic! (not a compile error). borrow() returns Ref<T> (Deref to &T), and borrow_mut() returns RefMut<T> (Deref to &mut T). These guards are dropped when they go out of scope, releasing the internal borrow count.

RefCell is not Sync—it cannot be shared across threads.

Mutex: Interior Mutability + Send + Locking

use std::sync::Mutex;
let m = Mutex::new(42);
let mut guard = m.lock().unwrap();   // Block until lock is acquired
*guard += 1;                         // Access internal value via DerefMut
drop(guard);                         // Release lock (or rely on scope exit)

Mutex combines RefCell's interior mutability + OS-level locking + Send guarantees. Under the hood, it uses UnsafeCell + an OS mutex (pthread_mutex_t on Linux). lock() blocks the current thread until the lock is acquired, and try_lock() is non-blocking, immediately returning Option<MutexGuard>.

Comparison of the Three

CellRefCellMutex
Interior MutabilityYYY
Returns ReferenceN (copy only)Y (Ref/RefMut)Y (MutexGuard)
Conflict HandlingN/Apanicblock
Thread-safeNNY
Check TimingCompile-time (no borrow)Runtime (panic)Runtime (block)

References

  • Rust Book: Chapter 15.5
  • Rustonomicon: UnsafeCell (the primitive behind all interior mutability)

Keywords: Cell, RefCell, Mutex, interior mutability, UnsafeCell, borrow_mut, runtime borrow check