【Rust】遅延初期化を行う LazyCell と LazyLock


LazyCell と LazyLock は遅延初期化を提供する構造体で、Rust 1.80.0 で安定版となった。

LazyLock はスレッドセーフであり、static 変数を遅延初期化できる。


LazyCell

LazyCell は初回アクセス時に指定された関数 F で値を初期化し、それ以降は初期化済みの値を利用できる。

以下のような構造体として提供されている。

pub struct LazyCell<T, F = fn() -> T> {
    state: UnsafeCell<State<T, F>>,
}

単純に 3 で遅延初期化する例としては以下のようになる。

use std::cell::LazyCell;
let lazy: LazyCell<i32> = LazyCell::new(|| 3);
println!("{}", *lazy);


Deref を以下のように実装しているため、*lazy の参照外しで値が初期化される。

impl<T, F: FnOnce() -> T> Deref for LazyCell<T, F> {
    type Target = T;
    #[inline]
    fn deref(&self) -> &T {
        LazyCell::force(self)
    }
}

実際の初期化処理は、LazyCell::force() で、以下のように状態に応じて初期化処理の実行制御が行われる。

pub fn force(this: &LazyCell<T, F>) -> &T {
    let state = unsafe { &*this.state.get() };
    match state {
        State::Init(data) => data,
        State::Uninit(_) => unsafe { LazyCell::really_init(this) },
        State::Poisoned => panic!("LazyCell has previously been poisoned"),
    }
}


Rust 1.70.0 で安定化された std::cell::OnceCellstd::sync::OnceLock と似ているが、OnceCell では get_or_init() で都度初期化関数を指定する必要がある。

use std::cell::OnceCell;
let cell = OnceCell::new();

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

OnceCell は値の書き込みが1回しかできない同期プリミティブ。


LazyLock

LazyLockSync トレイトを実装しているため、スレッドセーフに利用できる。

以下のように static 変数を遅延初期化することが可能となる。

use std::sync::LazyLock;
use std::time::Instant;

static LAZY_TIME: LazyLock<Instant> = LazyLock::new(Instant::now);

fn main() {
    let start = Instant::now();
    std::thread::scope(|s| {
        s.spawn(|| {
            println!("Thread lazy time is {:?}", LAZY_TIME.duration_since(start));
        });
        println!("Main lazy time is {:?}", LAZY_TIME.duration_since(start));
    });
}

LazyLock 値への初回アクセス時にスレッドセーフに初期化されるため、それぞれのスレッドで同じ経過時間が表示される。

LazyCell!Sync トレイトを実装するため、上記の使い方はできない。