19.6 Interior Mutability: Cell<T>, RefCell<T>, OnceCell<T>

Rust’s borrowing rules are strict: you cannot have mutable access (&mut T) at the same time as any other reference (&T or &mut T) to the same data. This is checked at compile time and prevents data races. However, sometimes this is too restrictive. The interior mutability pattern allows mutation through a shared reference (&T), moving the borrowing rule checks from compile time to runtime or using specific mechanisms for simple types.

These types reside in the std::cell module and are generally intended for single-threaded use cases.

19.6.1 Cell<T>: Simple Value Swapping (for Copy types)

Cell<T> offers interior mutability for types T that implement the Copy trait (primitive types like i32, f64, bool, tuples/arrays of Copy types, and simple structs composed of Copy types).

  • Operations: Provides get() which copies the current value out, and set(value) which replaces the internal value. It also offers replace() and swap().
  • Safety Mechanism: No runtime borrowing checks occur. Safety relies on the Copy nature of T. Since you only ever get copies or replace the value wholesale, you can’t create dangling references to the interior data through the Cell’s API.
  • Overhead: Very low overhead, typically compiles down to simple load/store instructions.

Example:

use std::cell::Cell;

fn main() {
    // `i32` implements Copy
    let shared_counter = Cell::new(0);

    // Can mutate through the shared reference `&shared_counter`
    let current = shared_counter.get();
    shared_counter.set(current + 1);

    shared_counter.set(shared_counter.get() + 1); // Increment again

    println!("Counter value: {}", shared_counter.get()); // Output: 2
}

19.6.2 RefCell<T>: Runtime Borrow Checking

For types that are not Copy, or when you need actual references (&T or &mut T) to the internal data rather than just copying/replacing it, RefCell<T> is the appropriate choice.

  • Mechanism: Enforces Rust’s borrowing rules (one mutable borrow XOR multiple immutable borrows) at runtime. It keeps track of the current borrow state internally.
  • Operations:
    • borrow(): Returns a smart pointer wrapper (Ref<T>) providing immutable access (&T). Increments an internal immutable borrow count. Panics if there’s an active mutable borrow.
    • borrow_mut(): Returns a smart pointer wrapper (RefMut<T>) providing mutable access (&mut T). Marks an internal flag indicating a mutable borrow. Panics if there are any other active borrows (mutable or immutable).
  • Safety Mechanism: Runtime checks. If borrowing rules are violated, the program panics immediately, preventing data corruption or undefined behavior.
  • Overhead: Higher than Cell<T> due to runtime tracking of borrow state (counts/flags).

Example:

use std::cell::RefCell;

fn main() {
    // Vec<i32> is not Copy
    let shared_list = RefCell::new(vec![1, 2, 3]);

    // Get an immutable borrow
    {
        // `borrow()` returns Ref<Vec<i32>>, which derefs to &Vec<i32>
        let list_ref = shared_list.borrow();
        println!("First element: {}", list_ref[0]);
        // list_ref goes out of scope here, releasing the immutable borrow
    }

    // Get a mutable borrow
    {
        // `borrow_mut()` returns RefMut<Vec<i32>>, which derefs to &mut Vec<i32>
        let mut list_mut_ref = shared_list.borrow_mut();
        list_mut_ref.push(4);
        // list_mut_ref goes out of scope here, releasing the mutable borrow
    }

    println!("Current list: {:?}", shared_list.borrow());

    // Example of runtime panic: Uncommenting the lines below would cause a panic
    // let _first_borrow = shared_list.borrow();
    // let _second_borrow_mut = shared_list.borrow_mut();
    // PANIC! Cannot mutably borrow while immutably borrowed.
}

19.6.3 Combining Rc<T> and RefCell<T>

A very common pattern is Rc<RefCell<T>>. This allows multiple owners (Rc) to share access to data that can also be mutated (RefCell) within a single thread, deferring borrow checks to runtime.

Example: Simulating a graph node that can be shared and whose children can be modified.

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

#[derive(Debug)]
struct Node {
    value: i32,
    // Children owned via Rc, but Vec is mutable via RefCell
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let root = Rc::new(Node {
        value: 10,
        children: RefCell::new(vec![]),
    });

    let child1 = Rc::new(Node { value: 11, children: RefCell::new(vec![]) });
    let child2 = Rc::new(Node { value: 12, children: RefCell::new(vec![]) });

    // Mutate the children Vec through the RefCell, even though `root` is shared via Rc
    // Obtain a mutable borrow of the Vec inside the RefCell
    root.children.borrow_mut().push(Rc::clone(&child1));
    root.children.borrow_mut().push(Rc::clone(&child2));
    // Mutable borrows are released here

    println!("Root node: {:?}", root);
    println!("Child1 strong count: {}", Rc::strong_count(&child1));
    // Output: 2 (root.children + child1 var)
}

std::cell::OnceCell<T> provides a cell that can be written to exactly once. It’s useful for lazy initialization or setting global configuration within a single thread. After the first successful write, subsequent attempts fail silently. get() returns an Option<&T>.

Related types like std::sync::OnceLock (thread-safe) or types in crates like once_cell provide convenient wrappers for computing a value on first access (lazy initialization).

Example (OnceCell):

use std::cell::OnceCell;

fn main() {
    let config: OnceCell<String> = OnceCell::new();

    // Try to get the value before setting - returns None
    assert!(config.get().is_none());

    // Initialize the config
    let result = config.set("Initial Value".to_string());
    assert!(result.is_ok());

    // Try to get the value now - returns Some(&String)
    println!("Config value: {}", config.get().unwrap());

    // Attempting to set again fails (returns Err containing the value we tried to set)
    let result2 = config.set("Second Value".to_string());
    assert!(result2.is_err());
    println!("Config value is still: {}", config.get().unwrap());
    // Remains "Initial Value"
}

Summary of Single-Threaded Interior Mutability:

  • Cell<T>: For Copy types, minimal overhead, use when simple get/set/swap is sufficient.
  • RefCell<T>: For non-Copy types or when references (&T/&mut T) are needed. Enforces borrow rules at runtime (panics on violation). Use when mutation is needed via a shared reference.
  • OnceCell<T>: For write-once, read-many scenarios like lazy initialization.
  • These are not thread-safe. For concurrent scenarios, use their std::sync counterparts (Mutex, RwLock, OnceLock).