Lazycell

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.

95. LazyLock::get — Peek at a Lazy Value Without Initializing It

Wanted to know whether a LazyLock has been initialized yet — without causing the initialization by touching it? Rust 1.94 stabilises LazyLock::get and LazyCell::get, which return Option<&T> and leave the closure untouched if it hasn’t fired.

The old pain

Any access that goes through Deref forces the closure to run. So the moment you do *CONFIG — or anything that implicitly derefs — the lazy becomes eager. Before 1.94 there was no way to ask “has this been initialized?” without tripping that wire.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use std::sync::LazyLock;

static CONFIG: LazyLock<Vec<String>> = LazyLock::new(|| {
    // Imagine this is slow: reads a file, hits the network, etc.
    vec!["debug".into(), "verbose".into()]
});

fn main() {
    // No way to peek without paying the init cost
    let _ = CONFIG.len(); // forces init
    assert_eq!(CONFIG.len(), 2);
}

Handy enough, but if you want a metric like “was the config ever read?” you’d have to wrap the whole thing in your own AtomicBool.

The fix: LazyLock::get

Called as an associated function (LazyLock::get(&lock)), it returns Option<&T> without touching the closure. None means the lazy is still pending. Some(&value) means someone already forced it.

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

static CONFIG: LazyLock<Vec<String>> = LazyLock::new(|| {
    vec!["debug".into(), "verbose".into()]
});

fn main() {
    // Not yet initialised — get returns None
    assert!(LazyLock::get(&CONFIG).is_none());

    // Force init via normal deref
    assert_eq!(CONFIG.len(), 2);

    // Now get returns Some(&value)
    let peek = LazyLock::get(&CONFIG).unwrap();
    assert_eq!(peek.len(), 2);
}

Note the call style: LazyLock::get(&CONFIG), not CONFIG.get(). That’s deliberate — method-lookup on the lock itself would go through Deref, which is exactly the thing we’re trying to avoid.

Same story for LazyCell

LazyCell is the single-threaded cousin and gets the same treatment:

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

fn main() {
    let greeting = LazyCell::new(|| "Hello, Rust!".to_uppercase());

    // Not forced yet
    assert!(LazyCell::get(&greeting).is_none());

    // Deref triggers the closure
    assert_eq!(*greeting, "HELLO, RUST!");

    // Now we can peek without re-running anything
    assert_eq!(LazyCell::get(&greeting), Some(&"HELLO, RUST!".to_string()));
}

Where this shines

Two patterns fall out naturally.

Metrics and diagnostics. You want to log “did we ever load the config?” at shutdown without accidentally loading it just to find out:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use std::sync::LazyLock;

static CACHE: LazyLock<Vec<u64>> = LazyLock::new(|| (0..1000).collect());

fn was_cache_used() -> bool {
    LazyLock::get(&CACHE).is_some()
}

fn main() {
    assert!(!was_cache_used());
    let _ = CACHE.first(); // touch it
    assert!(was_cache_used());
}

Tests. Assert that the lazy init didn’t happen on a code path that shouldn’t need it — a guarantee that was basically impossible to write before.

When to reach for it

Use LazyLock::get / LazyCell::get whenever you need to ask about initialization state without causing it. For everything else, just deref as usual — that’s still the one-liner that Just Works.

Stabilised in Rust 1.94 (March 2026).