19.6 Interior Mutability with Cell<T>, RefCell<T>, and OnceCell<T>

Rust’s borrowing rules ensure memory safety by preventing mutable access through immutable references at compile time. This prevents many errors but can be overly restrictive in certain advanced scenarios. Sometimes you know that mutating data behind an immutable reference is safe, but the compiler’s static analysis cannot prove it.

Interior mutability allows you to safely mutate data even when it’s behind an immutable reference, using runtime checks rather than compile-time checks. Types like Cell<T>, RefCell<T>, and OnceCell<T> provide this capability. For shared mutable structures, Rc<RefCell<T>> combines shared ownership and interior mutability, enabling flexible data structures like dynamically modifiable trees or graphs.

19.6.1 Cell<T>: Copy-Based Interior Mutability

Cell<T> is the simplest. It provides interior mutability for types that implement Copy. Instead of borrowing, Cell<T> moves or replaces values directly, avoiding runtime borrow checks entirely.

Example:

use std::cell::Cell;

fn main() {
    let cell = Cell::new(42);
    let a = &cell;
    let b = &cell;
    a.set(100);
    b.set(1000);
    println!("Value: {}", a.get());
}

Use Cell<T> when working with small, Copy types that need occasional updates without borrowing references out of the cell.

19.6.2 RefCell<T>: Runtime Borrow Checking

RefCell<T> allows interior mutability for more complex, non-Copy types. Unlike Cell<T>, RefCell<T> supports borrowing. The borrowing rules are enforced at runtime. Violations cause a panic rather than a compile-time error.

Example:

use std::cell::RefCell;

fn main() {
    let cell = RefCell::new(42);
    {
        *cell.borrow_mut() += 1;
        println!("Value: {}", cell.borrow());
    }
    {
        let mut bm = cell.borrow_mut();
        // println!("Value: {}", cell.borrow()); // Would panic at runtime
        *bm += 1;
    }
    {
        let _borrow1 = cell.borrow();
        let _borrow2 = cell.borrow();
        // let _mutable_borrow = cell.borrow_mut(); // Panics at runtime
    }
}

Use RefCell<T> when you need to mutate data while only having an immutable reference, or when working with Rc<T> to build shared, mutable structures in a single-threaded context.

19.6.3 Combining Rc<T> and RefCell<T>

Rc<RefCell<T>> enables shared ownership and runtime-checked interior mutability. This pattern is common in data structures that must be dynamically updated while shared by multiple owners.

Example: Tree Structure

use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
    value: i32,
    children: Vec<Rc<RefCell<Node>>>,
}

fn main() {
    let root = Rc::new(RefCell::new(Node { value: 1, children: vec![] }));
    let child1 = Rc::new(RefCell::new(Node { value: 2, children: vec![] }));
    let child2 = Rc::new(RefCell::new(Node { value: 3, children: vec![] }));
    root.borrow_mut().children.push(Rc::clone(&child1));
    root.borrow_mut().children.push(Rc::clone(&child2));
    child1.borrow_mut().value = 42;
    println!("{:#?}", root);
}

19.6.4 OnceCell<T>: Single Initialization

OnceCell<T> allows you to set a value once and then access it immutably afterward. This is useful for lazy initialization or scenarios where you only want to assign a value once at runtime. A thread-safe variant (std::sync::OnceCell) exists for multi-threaded contexts.

Example:

use std::cell::OnceCell;

fn main() {
    let cell = OnceCell::new();
    cell.set(42).expect("Failed to set value");
    println!("Value: {}", cell.get().expect("Not initialized"));
    // Attempting to set twice would panic
}

19.6.5 Summary of Interior Mutability

  • Cell<T>: Low-overhead, Copy-only interior mutability with no runtime borrowing checks.
  • RefCell<T>: Runtime-checked borrowing, enabling mutation of complex types even when accessed via immutable references.
  • OnceCell<T>: Single initialization with immutable access thereafter, useful for lazy setup.
  • Rc<RefCell<T>: Combines shared ownership and interior mutability for flexible data structures.

Interior mutability provides an escape hatch for advanced scenarios where Rust’s default static checks are too restrictive, while still preserving safety.