Caches, counters, lazy globals and shared state all rely on interior mutability, and choosing the right cell avoids panics and locks.
Normally &T means "read-only". Interior mutability is the controlled exception: types that allow mutation through a shared reference by enforcing safety some other way. They all build on `UnsafeCell<T>`, the only legal way in Rust to mutate data reachable through &; every other approach is undefined behaviour.
Single-threaded cells (not Sync):
- `Cell<T>`: no borrowing at all. You
geta copy (forCopytypes),set,replaceortakethe whole value. Zero overhead, can never panic. Ideal for counters and flags. - `RefCell<T>`: hands out
Ref/RefMutguards and tracks borrows at runtime, panicking on conflicts. Use when you need references into the value. - `OnceCell<T>`: written at most once;
get_or_init(|| ...)computes on first use. Good for lazily computed fields. - `LazyCell<T, F>`: an
OnceCellbundled with its initializer.
Thread-safe equivalents:
- `OnceLock<T>` (since 1.70) and `LazyLock<T>` (since 1.80) for lazily initialised globals:
static CONFIG: LazyLock<Config> = LazyLock::new(load);. They replace thelazy_staticandonce_cellcrates. - atomics (
AtomicUsize,AtomicBool) are the thread-safeCellfor integers. - `Mutex<T>`/`RwLock<T>` are the thread-safe
RefCell: they block instead of panicking.
Avoid static mut: in the 2024 edition, taking a reference to one is denied by default (the static_mut_refs lint), because it invites data races and aliasing bugs. Use an atomic, a Mutex, or a LazyLock instead.
Interior mutability is a tool for caches, memoization, reference-counted graphs and statistics behind &self APIs. If you can restructure so that one owner holds &mut, that is usually simpler.
Example
use std::{cell::{Cell, OnceCell}, collections::HashMap, sync::LazyLock};
static UNITS: LazyLock<HashMap<&str, f64>> = LazyLock::new(|| {
println!("(building table once)");
HashMap::from([("km", 1000.0), ("m", 1.0)])
});
struct Doc {
text: String,
reads: Cell<u32>, // mutated through &self
word_count: OnceCell<usize>, // computed lazily, once
}
impl Doc {
fn words(&self) -> usize {
self.reads.set(self.reads.get() + 1);
*self.word_count.get_or_init(|| self.text.split_whitespace().count())
}
}
fn main() {
let d = Doc { text: "a b c".into(), reads: Cell::new(0), word_count: OnceCell::new() };
println!("{} {} reads={}", d.words(), d.words(), d.reads.get()); // 3 3 reads=2
println!("{} {}", UNITS["km"], UNITS["m"]); // (building table once) then 1000 1
}Remember
- All interior mutability is built on
UnsafeCell Cellfor smallCopyvalues,RefCellfor borrowed access,OnceCellfor compute-once- Use
LazyLock/OnceLock, atomics orMutexfor globals; avoidstatic mut
Watch out for
- Using
RefCellwhereCellwould do and paying for (and risking) runtime borrow panics.