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

Rust’s compile-time guarantees normally prohibit mutating data through an immutable reference. This is essential for safety but can occasionally be too restrictive when you know a certain mutation is safe.

Interior mutability provides a solution by allowing controlled mutation at runtime, guarded by checks or specialized mechanisms. The most common types for this purpose are:

  • Cell<T>
  • RefCell<T>
  • OnceCell<T> (with a corresponding thread-safe version in std::sync)

19.6.1 Cell<T>: Copy-Based Interior Mutability

Cell<T> replaces values rather than borrowing them. It works only for types that implement Copy. There are no runtime borrow checks; you can simply set or get the stored value.

Example:

use std::cell::Cell;

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

19.6.2 RefCell<T>: Runtime Borrow Checking

For non-Copy types or more complex borrowing patterns, RefCell<T> enforces borrow rules at runtime. If you violate Rust’s normal borrowing constraints (e.g., attempting to borrow mutably while another borrow exists), your program will panic.

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();
        *bm += 1;
        // println!("Value: {}", cell.borrow()); // This would panic at runtime
    }
}

19.6.3 Combining Rc<T> and RefCell<T>

A common pattern is Rc<RefCell<T>>: multiple owners of data that requires mutation. This is particularly valuable in graph or tree structures with dynamic updates:

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 initializing data exactly once, then accessing it immutably afterward. A thread-safe variant (std::sync::OnceCell) is available for concurrent scenarios.

Example:

use std::cell::OnceCell;

fn main() {
    let cell = OnceCell::new();
    cell.set(42).unwrap();
    println!("Value: {}", cell.get().unwrap());
    // Attempting to set a second time would panic
}

Summary of Interior Mutability Tools

  • Cell<T>: For Copy types only, provides set/get operations without borrow checking.
  • RefCell<T>: For complex mutation needs with runtime borrow checking.
  • OnceCell<T>: Allows a single initialization followed by immutable reads.
  • Rc<RefCell<T>>: Frequently used for shared, mutable data in single-threaded contexts.