Interior Mutability

LazyCell<T>

std::cell::LazyCell<T, F = fn() -> T> (stabilized in Rust 1.80) is a single-threaded lazy wrapper that bundles an initialization closure F directly with a write-once storage slot and automatically runs the closure the first time the cell is dereferenced (*lazy or LazyCell::force(&lazy)).

LazyCell<T, F> vs. OnceCell<T>

While OnceCell<T> starts empty and requires passing an initialization closure to .get_or_init(|| ...) at the point of access, LazyCell<T, F> takes the initialization closure at construction time (LazyCell::new(|| ...)) and implements Deref<Target = T>, so callers access it like a normal value or smart pointer.

LazyCell<T, F> Internal State Transition:

  Before First Deref:                 On First Deref (`*lazy`):
  +---------------------------+       +---------------------------+
  | State::Uninit(f: F)       | ----> | Runs `let val = f();`     |
  +---------------------------+       | Transitions to:           |
                                      | State::Init(val: T)       |
                                      +-------------+-------------+
                                                    |
                                                    v
                                      Subsequent Derefs return `&val` directly
Feature OnceCell<T> LazyCell<T, F>
Where initializer is provided At access time (.get_or_init(f)) or .set(val) At construction time (LazyCell::new(f))
Can different call sites pass different initializers? Yes No (single fixed initializer F)
Can initializer access &self of enclosing struct? Yes (self.cell.get_or_init(|| self.other_field)) Only variables captured when LazyCell was constructed
Implements Deref<Target = T>? No (use .get() or .get_or_init()) Yes (*lazy and .method() initialize automatically)

Using LazyCell<T> for On-Demand Computation

use std::cell::LazyCell;
use std::collections::HashMap;

fn main() {
    let multiplier = 10;

    let lookup = LazyCell::new(|| {
        println!("  [initializing lookup table...]");
        HashMap::from([
            ("alpha", 1 * multiplier),
            ("beta", 2 * multiplier),
            ("gamma", 3 * multiplier),
        ])
    });

    println!("lookup created (not yet initialized)");
    // First dereference runs the closure:
    println!("alpha = {}", lookup["alpha"]);
    // Second dereference reuses the initialized HashMap:
    println!("gamma = {}", lookup["gamma"]);
}
lookup created (not yet initialized)
  [initializing lookup table...]
alpha = 10
gamma = 30

Storing LazyCell<T> Inside a Struct

Because every closure in Rust has a unique anonymous type, storing a LazyCell in a struct field defaults the second type parameter F to a function pointer fn() -> T (LazyCell<T> is an alias for LazyCell<T, fn() -> T>), or uses Box<dyn FnOnce() -> T> when capturing environment state:

use std::cell::LazyCell;

struct Environment {
    // Uses the default `F = fn() -> String` type parameter:
    hostname: LazyCell<String>,
}

fn default_hostname() -> String {
    println!("  [resolving hostname...]");
    String::from("node-01.internal")
}

fn main() {
    let env = Environment {
        hostname: LazyCell::new(default_hostname),
    };

    println!("before access");
    println!("host: {}", &*env.hostname);
    println!("host again: {}", &*env.hostname);
}
before access
  [resolving hostname...]
host: node-01.internal
host again: node-01.internal

Re-Entrancy Panic Poisoning

If the initialization closure of a LazyCell recursively dereferences the same LazyCell before initialization completes (or if the initialization closure panics), LazyCell transitions to a poisoned state and panics with "LazyCell instance has previously been poisoned" on subsequent accesses.