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, andset(value)which replaces the internal value. It also offersreplace()andswap(). - Safety Mechanism: No runtime borrowing checks occur. Safety relies on the
Copynature ofT. Since you only ever get copies or replace the value wholesale, you can’t create dangling references to the interior data through theCell’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) }
19.6.4 OnceCell<T> / LazyCell<T> (and related types): One-Time Initialization
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>: ForCopytypes, minimal overhead, use when simple get/set/swap is sufficient.RefCell<T>: For non-Copytypes 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::synccounterparts (Mutex,RwLock,OnceLock).