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()returnsNone. - 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 returnsErr(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&Treferences 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([])