152. RefCell<T> — When You Need to Actually Borrow the Inside
This morning’s Cell<T> only lets you swap whole values in and out. The moment you want to call .push() on the Vec inside, or hand out a &str slice of the String inside, you need a real &mut — and that’s exactly what RefCell<T> gives you, just with the aliasing rules checked at runtime instead of compile time.
The pain: Cell can’t lend the inside out
Cell<T> is great until you need to do anything to the value in place:
| |
The take/mutate/replace dance works but it’s noisy, and any code that runs between take and set sees an empty Vec. For non-trivial data structures — a cache, an arena, a graph node — that “hole” is unworkable.
The fix: borrow() and borrow_mut()
RefCell<T> hands out actual references through &self. borrow() gives you a Ref<T> (deref to &T), borrow_mut() gives you a RefMut<T> (deref to &mut T):
| |
Both methods take &self, so this works behind an Rc, inside an Fn closure, in any field of a struct you only have a shared reference to. That’s the whole point of interior mutability — &self outside, &mut inside.
The invariant: same rules, just checked at runtime
RefCell enforces the exact same aliasing rule the compiler enforces statically: many &T xor one &mut T. Try to break it and it panics:
| |
The panic message is already borrowed: BorrowMutError (or BorrowError). If you can’t guarantee statically that the borrows are well-nested, use try_borrow / try_borrow_mut — they return a Result instead of panicking, which is what you want inside a Drop impl or any code path that already might be re-entering itself.
A real pattern: shared mutable state through Rc<RefCell<_>>
The canonical use is sharing one piece of mutable state between several owners on a single thread — a cache, a config, an observer list:
| |
Keep borrow_mut() scopes short — release the RefMut (let it drop) before calling any code that might try to borrow the same cell again. The most common “works in tests, panics in prod” bug with RefCell is a long-lived RefMut colliding with a callback that re-enters.
When to pick RefCell vs Cell
Cell<T> if swapping or copying whole values is enough — counters, flags, small Copy state. Zero overhead, no panic risk.
RefCell<T> when the inside is a real data structure you want to call methods on in place: Vec, String, HashMap, your own structs. You pay one extra word for the borrow flag and runtime checks, but you get the full &T / &mut T API back.
For multi-threaded shared state, neither of these works — both are !Sync. Reach for Mutex<T> (covered in tomorrow’s morning bite) or RwLock<T> instead.