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).

  • 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.
  • Operations:
    • borrow(): Returns a smart pointer wrapper (Ref<T>) providing immutable access (&T). Tracks the number of active immutable borrows. Panics if there’s an active mutable borrow.
    • borrow_mut(): Returns a smart pointer wrapper (RefMut<T>) providing mutable access (&mut T). Tracks if there’s an active 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.
  • Overhead: Higher than Cell<T> due to runtime tracking of borrow counts.

Example:

use std::cell::RefCell;

fn main() {
    let shared_list = RefCell::new(vec![1, 2, 3]);

    // Get an immutable borrow
    {
        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
    {
        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.

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: 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
    root.children.borrow_mut().push(Rc::clone(&child1));
    root.children.borrow_mut().push(Rc::clone(&child2));

    println!("Root node: {:?}", root);
    println!("Child1 strong count: {}", Rc::strong_count(&child1)); // Output: 2 (root + 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. After the first successful write, subsequent attempts fail. get() returns an Option<&T>.

Related types like std::lazy::LazyCell (or crates like once_cell) provide convenient wrappers for computing a value on first access.

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
    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, std::sync::OnceLock).