Interior Mutability

OnceCell<T>

std::cell::OnceCell<T> is a single-threaded interior mutability container that starts empty and can be written to at most once. Unlike RefCell<T>, which returns a temporary Ref<'_, T> guard and tracks a dynamic borrow counter, OnceCell<T> returns a direct, guard-free shared reference &T tied to the lifetime of &OnceCell<T>—because once initialized, the inner value can never be overwritten or dropped while shared references exist.

Why OnceCell<T> Can Return &T Directly Without a Guard

In Cell<T> and RefCell<T>, subsequent calls through &self can overwrite or mutate the stored value, which would invalidate any previously returned &T. In OnceCell<T>:

  • Before initialization, .get() returns None.
  • Calling .set(value) or .get_or_init(f) transitions the cell from empty to initialized.
  • Once initialized, any further .set(new_value) through &OnceCell<T> fails and returns Err(new_value) without modifying the existing value.
  • Because the initialized value is pinned to the lifetime of the OnceCell<T> (unless exclusive &mut OnceCell<T> is held), handing out plain &T references is completely sound with zero guard overhead.
OnceCell<T> State Machine (via shared &OnceCell<T>):

   OnceCell::new()
         |
         v
   [ Empty (None) ]  --- .get() ---> None
         |
         | .set(val) or .get_or_init(|| val)
         v
   [ Initialized(T) ] --- .get() ---> Some(&T) (Direct reference, no Ref<T> guard!)
         |
         +--- subsequent .set(v2) ---> Err(v2) (Existing &T references remain valid!)

Core OnceCell<T> Methods

Method Receiver Return Type Behavior
OnceCell::new() — OnceCell<T> Creates an empty cell (const fn)
OnceCell::from(val) — OnceCell<T> Creates an already-initialized cell
.get() &self Option<&T> Returns Some(&T) if initialized, or None if empty
.set(val) &self Result<(), T> Initializes the cell if empty (Ok(())), or returns Err(val) if already set
.get_or_init(f) &self &T Returns existing &T, or runs closure f() once to initialize and return &T
.get_mut() &mut self Option<&mut T> Grants mutable access when caller holds exclusive &mut OnceCell<T>
.take() &mut self Option<T> Moves the value out, resetting the cell to empty (requires &mut self)
.into_inner() self Option<T> Consumes the OnceCell and returns its value

Lazy Field Memoization with OnceCell<T>

OnceCell<T> is ideal for struct fields whose values are expensive to compute and depend on other fields of &self (unlike LazyCell<T>, whose initialization closure cannot access &self at call time):

use std::cell::OnceCell;

struct Document {
    body: String,
    word_list: OnceCell<Vec<String>>,
}

impl Document {
    fn new(body: &str) -> Self {
        Self {
            body: body.to_string(),
            word_list: OnceCell::new(),
        }
    }

    fn words(&self) -> &[String] {
        self.word_list.get_or_init(|| {
            println!("  [computing word list once...]");
            self.body
                .split_whitespace()
                .map(|w| w.to_lowercase())
                .collect()
        })
    }
}

fn main() {
    let doc = Document::new("Rust OnceCell caches computed slices cleanly");

    println!("initialized before call? {}", doc.word_list.get().is_some());
    println!("first call:  {:?}", doc.words());
    println!("second call: {:?}", doc.words());
    println!("manual set on initialized cell: {:?}", doc.word_list.set(vec![]));
}
initialized before call? false
  [computing word list once...]
first call:  ["rust", "oncecell", "caches", "computed", "slices", "cleanly"]
second call: ["rust", "oncecell", "caches", "computed", "slices", "cleanly"]
manual set on initialized cell: Err([])