Oncecell

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use std::cell::RefCell;

struct DocSlow {
    raw: String,
    parsed: RefCell<Option<Vec<String>>>,
}

impl DocSlow {
    fn lines(&self) -> Vec<String> {
        let mut slot = self.parsed.borrow_mut();
        if slot.is_none() {
            *slot = Some(self.raw.lines().map(str::to_owned).collect());
        }
        slot.clone().unwrap() // can't return a borrow that escapes RefMut
    }
}

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
use std::cell::OnceCell;

struct Doc {
    raw: String,
    parsed: OnceCell<Vec<String>>,
}

impl Doc {
    fn new(s: &str) -> Self {
        Self { raw: s.to_owned(), parsed: OnceCell::new() }
    }

    fn lines(&self) -> &[String] {
        self.parsed
            .get_or_init(|| self.raw.lines().map(str::to_owned).collect())
    }
}

let doc = Doc::new("one\ntwo\nthree");
assert_eq!(doc.lines(), &["one", "two", "three"]);
assert_eq!(doc.lines().len(), 3); // cached — closure does not run again

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 || ...:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use std::cell::OnceCell;

let cell: OnceCell<String> = OnceCell::new();
assert_eq!(cell.get(), None);

cell.set("loaded".into()).unwrap();
assert_eq!(cell.get(), Some(&"loaded".to_string()));

// Second set is rejected — the cell is full.
assert!(cell.set("nope".into()).is_err());

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:

1
2
3
4
5
6
7
8
use std::cell::LazyCell;

let tags: LazyCell<Vec<&'static str>> = LazyCell::new(|| {
    vec!["rust", "interior-mutability"]
});

assert_eq!(tags.len(), 2);   // closure runs here
assert_eq!(tags[0], "rust"); // cached

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.