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, andset(value)
which replaces the internal value. It also offersreplace()
andswap()
. - Safety Mechanism: No runtime borrowing checks occur. Safety relies on the
Copy
nature 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.
- 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) }
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. 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>
: ForCopy
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
).