153. OnceCell<T> — Memoize Through &self Without Wrapping in RefCell
You have a parse(&self) -> &Heavy accessor that needs to compute once and cache. &self rules out a plain field assignment. Cell needs Copy. RefCell won’t lend the inside out past .borrow(). OnceCell<T> is the missing piece — write-once, &self API, hands back a real &T that lives as long as the cell.
The pain: &self memoization is awkward
Classic shape — an immutable-looking accessor that’s expensive on first call and free afterwards:
| |
Two problems. We .clone() on every call because a Ref<'_, T> can’t outlive the borrow() it came from. And Option<Vec<String>> plus runtime borrow checking is overkill for “set this exactly once.”
The fix: OnceCell::get_or_init
OnceCell<T> stores at most one value. get_or_init runs the closure the first time it’s called and returns &T ever after — and that &T is tied to the lifetime of &self, so you can hand it back without cloning:
| |
No Option, no clone, no borrow_mut. The closure fires exactly once even across multiple calls, and the returned slice is good for as long as the &Doc is.
When you want to decide later, not on first read
OnceCell doesn’t require a closure at the call site. Use set when initialization is driven by something outside the cell — a parsed CLI flag, a value computed by a sibling method, anything that doesn’t fit a self-contained || ...:
| |
set returns Err(value) on the second call so you get your input back instead of dropping it on the floor. Reach for set when initialization is driven from outside; reach for get_or_init when it isn’t.
LazyCell<T, F>: when the closure is fixed at construction
If you already know how to build the value when you create the cell, LazyCell bakes the closure in and skips the Option-style API. The first deref runs it:
| |
Rule of thumb: LazyCell when there is exactly one obvious way to compute the value and you want the cell to handle it; OnceCell when you need set from outside, or different get_or_init closures at different call sites.
Thread safety
Both types are !Sync — they’re the single-thread counterparts to OnceLock / LazyLock. If a static or a field shared across threads needs this pattern, swap to the sync versions. The API shape is intentionally the same; only the guarantees (and the cost) change.