Rust in Depth

interior mutability (Cell and RefCell)

The aliasing-XOR-mutability rule says you may mutate a value only through a unique &mut. But real programs sometimes genuinely need to change something through a shared reference — a node in a graph that several other nodes point at, a cache hidden inside an otherwise-read-only object, a reference-counted value bumping its count. The compiler cannot prove these safe at compile time, but they ARE safe if checked carefully. Interior mutability is the pattern that allows it: special wrapper types that let you mutate the value inside even when you only hold a &T to the wrapper, moving the borrow check from compile time to run time (or to hardware).

The two single-threaded tools are Cell and RefCell. A Cell<T> holds a value you can replace wholesale: you cannot get a reference into its contents, but you can .set(new) or .get() (for Copy types) or .replace(new), swapping the whole value atomically with respect to the borrow rules — no references into it ever exist, so there is nothing to alias. A RefCell<T> is richer: it lets you borrow the inner value, with .borrow() giving a shared guard and .borrow_mut() giving an exclusive one, and it enforces the SAME many-readers-or-one-writer rule the compiler normally checks — but at run time, by keeping a small borrow counter inside. If you call .borrow_mut() while a .borrow() is still live, RefCell does not silently corrupt; it panics with 'already borrowed'. So RefCell does not remove the rule; it moves the check from the compiler to a runtime counter, trading a compile error for a possible panic. The trade buys you flexibility the static checker could not allow.

Why this matters: interior mutability is how you build the data structures the borrow checker would otherwise forbid — shared graphs, trees with parent pointers, observer patterns — without dropping into unsafe yourself (these types use unsafe inside, audited once, so you do not have to). The honest costs are real: RefCell's runtime check can panic if your borrowing is wrong, whereas a static error never reaches production; and a Cell/RefCell is single-threaded only (neither is Sync), so for shared mutation ACROSS threads you need the synchronised family — Mutex and RwLock (which block other threads instead of panicking) and the Atomic types (lock-free for single values). The rule of thumb: prefer the compiler's static check; reach for Cell/RefCell when a legitimate pattern needs shared mutation on one thread; reach for Mutex/RwLock/Atomic when it crosses threads.

use std::cell::RefCell; let log = RefCell::new(Vec::new()); log.borrow_mut().push("first"); // mutate through a shared &, checked at run time log.borrow_mut().push("second"); println!("{:?}", log.borrow()); // ["first", "second"] // Violating the rule panics instead of corrupting: let a = log.borrow(); // shared borrow live... // let b = log.borrow_mut(); // panic: already borrowed: BorrowMutError

RefCell keeps the same many-readers-or-one-writer rule but checks it at run time, panicking on a violation instead of refusing to compile.

Interior mutability does not bypass the borrow rule — it relocates the check. RefCell can panic at run time where the compiler would have errored, and Cell/RefCell are single-threaded only; crossing threads needs Mutex, RwLock, or an Atomic instead.

Also called
CellRefCellruntime borrow checkingshared mutability內部可變性執行期借用檢查