151. Cell<T> — Interior Mutability Without the Borrow Checker Drama
You hand the same struct to two closures and both want to bump a counter. &mut fights you, RefCell introduces runtime borrow checks you don’t need — Cell<T> quietly mutates through a shared & reference, with zero overhead, as long as you only ever swap whole values in and out.
The pain: shared & and a counter
Two closures, one counter, one immutable reference:
| |
Fn closures only capture &, so a plain field is read-only. You could redesign for FnMut and a single owner, but the moment you have two observers or a callback registry, you need interior mutability.
The fix: Cell<T> mutates through &
| |
Cell::new wraps the value; get returns a copy (so T: Copy for that method); set overwrites. Both take &self — no &mut anywhere — which is why this works inside Fn closures, inside Rc, inside any structure that hands out shared references.
The invariant: you can never borrow the inside
This is the single rule that makes Cell<T> sound without runtime checks: you cannot get a reference to the value inside, only copies and swaps. There is no cell.as_ref(), no cell.deref(). The compiler enforces this — there’s nothing to alias, so the optimizer is free to assume the inner value can’t change underneath an outstanding &T.
That’s also why Cell<T> is !Sync — fine for one thread, but two threads racing on set would tear the value. For threads, reach for Atomic* (scalars) or Mutex<T> (the rest).
replace and take work for non-Copy types too
get requires T: Copy, but Cell works with String, Vec, anything — you just have to move the value out instead of copying it:
| |
This is the trick people miss: Cell<Vec<T>> is perfectly usable for a shared, append-only-ish buffer — you just swap the whole Vec in and out.
When to pick Cell vs RefCell
Cell<T> if you only need to swap whole values: counters, flags, configuration knobs, small Copy state, or any field where replace/take is enough. Zero runtime overhead, no panic risk.
RefCell<T> (tomorrow afternoon’s bite) if you need to borrow the inside — call &mut self methods on a Vec in place, hand a &str slice to a caller, anything where copying or swapping the whole value would be wrong. You pay for runtime borrow tracking and risk a panic, but you get back the ability to use the value normally.
Default to Cell when it fits — it almost always does for simple shared-state tweaks, and the “no inner references” rule turns out to be exactly what you wanted anyway.