Lazy Initialization

RecipeCratesCategories
stdstdcat-memory-management
once_cellstd once_cellcat-memory-management
lazy_staticlazy_staticcat-memory-management

Two key libraries:

  • once_cell: newer crate with more ergonomic API. Should be preferred for all new projects.
  • lazy_static: older crate. API is less convenient, but crate is stable and maintained.

The core functionality of once_cell is now included in the standard library with the remaining parts on track to be stabilised in future.

std

std cat-memory-management

OnceCell⮳ is a cell which can be written to only once.

The corresponding Sync version of OnceCell<T> is OnceLock<T>.

use std::cell::OnceCell;

fn main() {
    let cell = OnceCell::new();
    assert!(cell.get().is_none());

    let value: &String = cell.get_or_init(|| "Hello, World!".to_string());
    println!("{value}");
    assert_eq!(value, "Hello, World!");
    assert!(cell.get().is_some());
}

once_cell

once_cell once_cell-crates.io once_cell-github once_cell-lib.rs cat-memory-management cat-rust-patterns

once_cell⮳ provides two cell-like types, unsync::OnceCell and sync::OnceCell. A OnceCell might store arbitrary non-Copy types, can be assigned to at most once and provides direct access to the stored contents. The sync flavor is thread-safe. once_cell also has a once_cell::sync::Lazy⮳ type, build on top of OnceCell⮳:

use std::collections::HashMap;
use std::sync::Mutex;

use once_cell::sync::Lazy;

// Must be static, not const
static GLOBAL_DATA: Lazy<Mutex<HashMap<i32, String>>> = Lazy::new(|| {
    let mut m = HashMap::new();
    m.insert(13, "Spica".to_string());
    m.insert(74, "Hoyten".to_string());
    Mutex::new(m)
});

fn main() {
    println!("{:?}", GLOBAL_DATA.lock().unwrap());
}

lazy_static

lazy_static lazy_static-crates.io lazy_static-github lazy_static-lib.rs cat-no-std cat-memory-management cat-rust-patterns

use std::collections::HashMap;
use std::sync::Mutex;
use std::sync::MutexGuard;

use anyhow::anyhow;
use lazy_static::lazy_static;

// `lazy_static` allows you to define statically initialized values that are
// computed lazily at runtime. It can be particularly useful for initializing
// data that is expensive to compute or that needs to be shared across multiple
// threads.

lazy_static! {
    // - Any type within the macro needs to fulfill the Sync trait.
    // - If a type has a destructor, it will not run when the process exits.
    // - The `static ref` keywords are passed to the macro, they are not part of the language per se.

    // CONFIG is a lazily-initialized HashMap that holds configuration data.
    static ref CONFIG: HashMap<String, String> = {
        let mut m = HashMap::new();
        m.insert("host".to_string(), "localhost".to_string());
        m.insert("port".to_string(), "8080".to_string());
        m
    };
    // You can call a method or a function:
    static ref COUNT: usize = CONFIG.len();

    // COUNTER is a lazily-initialized Mutex-protected integer counter.
    // The Mutex ensures that the COUNTER can be safely incremented from multiple threads, if needed.
    static ref COUNTER: Mutex<i32> = Mutex::new(0);
}

fn main() -> anyhow::Result<()> {
    // Accessing the CONFIG.
    println!(
        "Host: {}",
        CONFIG.get("host").ok_or(anyhow!("No host key"))?
    );
    println!(
        "Port: {}",
        CONFIG.get("port").ok_or(anyhow!("No port key"))?
    );

    // The macro generates a unique type that implements `Deref<TYPE>` and
    // stores it in a static with name `NAME`. Here, we use `*` to dereference
    // and retrieve the inner value.
    println!("Count: {}", *COUNT);

    // Working with the COUNTER:
    // The (mutable) shared state within the Mutex can only be accessed once the
    // lock is held. Our non-atomic increment is safe because we're the only
    // thread which can access the shared state when the lock is held.
    // We `unwrap()` the return value to assert that we are not expecting
    // threads to ever fail while holding the lock.
    {
        let mut counter: MutexGuard<'_, i32> = COUNTER.lock().unwrap();
        *counter += 1;
        println!("Counter: {}", *counter);
    } // The lock is unlocked here

    // And again, on a different thread:
    std::thread::spawn(move || {
        let lock_result = COUNTER.try_lock();
        // If the lock cannot be acquired at this time (or is poisoned),
        // `Err` is returned.
        if let Ok(mut counter) = lock_result {
            *counter += 1;
            println!("Counter: {}", *counter);
        } else {
            println!("try_lock failed");
        }
    })
    .join()
    .expect("thread::spawn failed");

    Ok(())
}