Expand description
§Overview
once_cell
provides two new cell-like types,unsync::OnceCell
andsync::OnceCell
. AOnceCell
might store arbitrary non-Copy
types, canbe assigned to at most once and provides direct access to the storedcontents. The core API looksroughly like this (and there’s much moreinside, read on!):
impl<T> OnceCell<T> {const fnnew() -> OnceCell<T> { ... }fnset(&self, value: T) ->Result<(), T> { ... }fnget(&self) ->Option<&T> { ... }}
Note that, like withRefCell
andMutex
, theset
method requiresonly a shared reference. Because of the single assignment restrictionget
can return a&T
instead ofRef<T>
orMutexGuard<T>
.
Thesync
flavor is thread-safe (that is, implements theSync
trait),while theunsync
one is not.
§Recipes
OnceCell
might be useful for a variety of patterns.
§Safe Initialization of Global Data
usestd::{env, io};useonce_cell::sync::OnceCell;#[derive(Debug)]pub structLogger {// ...}staticINSTANCE: OnceCell<Logger> = OnceCell::new();implLogger {pub fnglobal() ->&'staticLogger { INSTANCE.get().expect("logger is not initialized") }fnfrom_cli(args: env::Args) ->Result<Logger, std::io::Error> {// ...}}fnmain() {letlogger = Logger::from_cli(env::args()).unwrap(); INSTANCE.set(logger).unwrap();// use `Logger::global()` from now on}
§Lazy Initialized Global Data
This is essentially thelazy_static!
macro, but without a macro.
usestd::{sync::Mutex, collections::HashMap};useonce_cell::sync::OnceCell;fnglobal_data() ->&'staticMutex<HashMap<i32, String>> {staticINSTANCE: OnceCell<Mutex<HashMap<i32, String>>> = OnceCell::new(); INSTANCE.get_or_init(|| {letmutm = HashMap::new(); m.insert(13,"Spica".to_string()); m.insert(74,"Hoyten".to_string()); Mutex::new(m) })}
There are also thesync::Lazy
andunsync::Lazy
convenience types tostreamline this pattern:
usestd::{sync::Mutex, collections::HashMap};useonce_cell::sync::Lazy;staticGLOBAL_DATA: Lazy<Mutex<HashMap<i32, String>>> = Lazy::new(|| {letmutm = HashMap::new(); m.insert(13,"Spica".to_string()); m.insert(74,"Hoyten".to_string()); Mutex::new(m)});fnmain() {println!("{:?}", GLOBAL_DATA.lock().unwrap());}
Note that the variable that holdsLazy
is declared asstatic
,notconst
. This is important: usingconst
instead compiles, but works wrong.
§General purpose lazy evaluation
Unlikelazy_static!
,Lazy
works with local variables.
useonce_cell::unsync::Lazy;fnmain() {letctx =vec![1,2,3];letthunk = Lazy::new(|| { ctx.iter().sum::<i32>() });assert_eq!(*thunk,6);}
If you need a lazy field in a struct, you probably should useOnceCell
directly, because that will allow you to accessself
duringinitialization.
usestd::{fs, path::PathBuf};useonce_cell::unsync::OnceCell;structCtx { config_path: PathBuf, config: OnceCell<String>,}implCtx {pub fnget_config(&self) ->Result<&str, std::io::Error> {letcfg =self.config.get_or_try_init(|| { fs::read_to_string(&self.config_path) })?;Ok(cfg.as_str()) }}
§Lazily Compiled Regex
This is aregex!
macro which takes a string literal and returns anexpression that evaluates to a&'static Regex
:
macro_rules! regex { ($re:literal $(,)?) => {{staticRE: once_cell::sync::OnceCell<regex::Regex> = once_cell::sync::OnceCell::new(); RE.get_or_init(|| regex::Regex::new($re).unwrap()) }};}
This macro can be useful to avoid the “compile regex on every loopiteration” problem.
§Runtimeinclude_bytes!
Theinclude_bytes
macro is useful to include test resources, but it slowsdown test compilation a lot. An alternative is to load the resources atruntime:
usestd::path::Path;useonce_cell::sync::OnceCell;pub structTestResource { path:&'staticstr, cell: OnceCell<Vec<u8>>,}implTestResource {pub const fnnew(path:&'staticstr) -> TestResource { TestResource { path, cell: OnceCell::new() } }pub fnbytes(&self) ->&[u8] {self.cell.get_or_init(|| {letdir = std::env::var("CARGO_MANIFEST_DIR").unwrap();letpath = Path::new(dir.as_str()).join(self.path); std::fs::read(&path).unwrap_or_else(|_err| {panic!("failed to load test resource: {}", path.display()) }) }).as_slice() }}staticTEST_IMAGE: TestResource = TestResource::new("test_data/lena.png");#[test]fntest_sobel_filter() {letrgb:&[u8] = TEST_IMAGE.bytes();// ...}
§lateinit
LateInit
type for delayed initialization. It is reminiscent of Kotlin’slateinit
keyword and allows construction of cyclic data structures:
useonce_cell::sync::OnceCell;pub structLateInit<T> { cell: OnceCell<T> }impl<T> LateInit<T> {pub fninit(&self, value: T) {assert!(self.cell.set(value).is_ok()) }}impl<T> DefaultforLateInit<T> {fndefault() ->Self{ LateInit { cell: OnceCell::default() } }}impl<T> std::ops::DerefforLateInit<T> {typeTarget = T;fnderef(&self) ->&T {self.cell.get().unwrap() }}#[derive(Default)]structA<'a> { b: LateInit<&'aB<'a>>,}#[derive(Default)]structB<'a> { a: LateInit<&'aA<'a>>}fnbuild_cycle() {leta = A::default();letb = B::default(); a.b.init(&b); b.a.init(&a);let_a =&a.b.a.b.a;}
§Comparison with std
!Sync types | Access Mode | Drawbacks |
---|---|---|
Cell<T> | T | requiresT: Copy forget |
RefCell<T> | RefMut<T> /Ref<T> | may panic at runtime |
unsync::OnceCell<T> | &T | assignable only once |
Sync types | Access Mode | Drawbacks |
---|---|---|
AtomicT | T | works only with certainCopy types |
Mutex<T> | MutexGuard<T> | may deadlock at runtime, may block the thread |
sync::OnceCell<T> | &T | assignable only once, may block the thread |
Technically, callingget_or_init
will also cause a panic or a deadlock ifit recursively calls itself. However, because the assignment can happen onlyonce, such cases should be more rare than equivalents withRefCell
andMutex
.
§Minimum Supportedrustc
Version
If only thestd
,alloc
, orrace
features are enabled, MSRV will beupdated conservatively, supporting at least latest 8 versions of the compiler.When using other features, likeparking_lot
, MSRV might be updated morefrequently, up to the latest stable. In both cases, increasing MSRV isnotconsidered a semver-breaking change and requires only a minor version bump.
§Implementation details
The implementation is based on thelazy_static
andlazy_cell
crates andstd::sync::Once
. In some sense,once_cell
just streamlines and unifiesthose APIs.
To implement a sync flavor ofOnceCell
, this crates uses either a customre-implementation ofstd::sync::Once
orparking_lot::Mutex
. This iscontrolled by theparking_lot
feature (disabled by default). Performanceis the same for both cases, but theparking_lot
basedOnceCell<T>
issmaller by up to 16 bytes.
This crate usesunsafe
.
§F.A.Q.
Should I use the sync or unsync flavor?
Because Rust compiler checks thread safety for you, it’s impossible toaccidentally useunsync
wheresync
is required. So, useunsync
insingle-threaded code andsync
in multi-threaded. It’s easy to switchbetween the two if code becomes multi-threaded later.
At the moment,unsync
has an additional benefit that reentrantinitialization causes a panic, which might be easier to debug than adeadlock.
Does this crate support async?
No, but you can useasync_once_cell
instead.
Does this crate supportno_std
?
Yes, but with caveats.OnceCell
is a synchronization primitive whichsemantically relies on blocking.OnceCell
guarantees that at most onef
will be called to compute the value. If two threads of execution callget_or_init
concurrently, one of them has to wait.
Waiting fundamentally requires OS support. Execution environment needs tounderstand who waits on whom to prevent deadlocks due to priority inversion.Youcould make code to compile by blindly using pure spinlocks, but theruntime behavior would be subtly wrong.
Given these constraints,once_cell
provides the following options:
- The
race
module provides similar, but distinct synchronization primitivewhich is compatible withno_std
. Withrace
, thef
function can becalled multiple times by different threads, but only one thread will winto install the value. critical-section
feature (with a-
, not_
) usescritical_section
to implement blocking.
Can I bring my own mutex?
There isgeneric_once_cell toallow just that.
Should I usestd::cell::OnceCell
,once_cell
, orlazy_static
?
If you can usestd
version (your MSRV is at least 1.70, and you don’t needextra featuresonce_cell
provides), usestd
. Otherwise, useonce_cell
.Don’t uselazy_static
.
§Related crates
- Most of this crate’s functionality is available in
std
starting withRust 1.70. Seestd::cell::OnceCell
andstd::sync::OnceLock
. - double-checked-cell
- lazy-init
- lazycell
- mitochondria
- lazy_static
- async_once_cell
- generic_once_cell (bringyour own mutex)