🎫

[Rust] once_cell にできて lazy-static にできないこと

2022/05/27に公開約3,700字

はじめに

こんな記事よりも一次情報を読んだほうがタメになります

https://docs.rs/once_cell/latest/once_cell/index.html

TL;DR

想定読者

  • 「Rust の定数周りがよくわからない」という方
  • lazy-staticは知ってるけどonce_cellは使ったことがない」という方

Rust における静的変数/定数

Rust では静的変数/定数をstatic/constキーワードで定義できますが

  1. static mutへのアクセスと再代入は unsafe である
  2. 値がコンパイル時に決まる必要がある

といった制約があり、ユースケースによっては力不足なことがあります。

具体的な例として、アプリケーション内で使用するミュータブルな設定値をグローバルに保持したい場合を考えます。

最も単純な実装としては

static mut CONFIG_DATA: Config = { ... };

のようなものが考えられますが、1. よりCONFIG_DATAへのアクセスにはunsafeが必要になるため避けるべきです。

あるいはMutexによる内部可変性を利用して

static CONFIG_DATA: Mutex<Config> = Mutex::new({ ... });

とする方法も考えられますが、Mutex::newはヒープアロケーションを必要とするので 2. によりコンパイルが通りません。

このようなときに Rust で広く用いられる方法は lazy-static クレートを用いて

use lazy_static::lazy_static;
lazy_static! {
    static ref CONFIG_DATA: Mutex<Config> = Mutex::new({ ... });
}

とするものです。
ここでCONFIG_DATAは実行時に初めてアクセスされたときに右辺の値を評価し、その値で初期化されます。このため動的な値でstaticを初期化することができるようになります。

once_cell vs. lazy-static

once_celllazy-staticはともに Rust でグローバルな静的変数/定数を定義するために使われるクレートです。もともとはlazy-staticがデファクトスタンダードとして広く使われてきましたが、その代替としてonce_cellが最近ではトレンドとなっています。

実際、once_cellを用いると先ほどのlazy_static!は次のように書き換えることができます。

use once_cell::sync::Lazy;
static CONFIG_DATA: Lazy<Mutex<Config>> = Lazy::new(|| Mutex::new({ ... }));

このようにLazyという型がつくのでCONFIG_DATAが実行時に遅延評価されることが分かりやすく、マクロがなくなってスッキリとした見た目になることからもこちらの方が好ましいです。

DL数の比較(参考)

https://crate-trends.vercel.app/lazy-static+once_cell

将来性

さらに Rust のstd/coreへの導入も予定されていて、すでにnighlyチャンネルでは#![features(once_cell)]を有効にすることで使用できます。

https://github.com/rust-lang/rust/issues/74465

once_cellのさらなる機能

once_celllazy-staticの単なる代替にとどまらず、さらに柔軟で便利なAPIを提供します。

グローバル変数を状況に応じて異なる型で初期化する

これがこの記事を書こうと思った最大の動機です。あるOSSの開発中にreviewerの方からアイデアをいただいてなるほどと思ったので紹介したいと思いました。

lazy_static!で定義された変数GLOBAL_DATAが、関数a()内で初期化されたときは型Ab()内で初期化されたときは型Bとなるように初期化したいとします。
もちろんRustは静的型付け言語なのでそのようは型は許可されません。よってまずは次のようなenumを定義することになります。

enum GlobalData {
   DataA(A),
   DataB(B),
}

そしてlazy_static!は次のようになりそうです。

use lazy_static::lazy_static;
lazy_static! {
    static ref GLOBAL_DATA: GlobalData = {
        if todo!("which am I called by a() or b()?") {
	    GlobalData::DataA({ ... })
	} else {
	    GlobalData::DataB({ ... })
	}
    };
}

しかしlazy_static!からはa()から呼ばれたのかb()から呼ばれたのかを判断する術がありません。無理やり解決するならばstatic mut CALLED_BY_A: boolのような変数を用意することができますがやはり unsafe です。困りました。

ここでonce_cellを使います。

use once_cell::sync::OnceCell;
static GLOBAL_DATA: OnceCell<GlobalData> = OnceCell::new();

fn a() -> &'static GlobalData {
    GLOBAL_DATA.get_or_init(|| {
        GlobalData::DataA({ ... })
    })
}

fn b() -> &'static GlobalData {
    GLOBAL_DATA.get_or_init(|| {
        GlobalData::DataB({ ... })
    })
}

OnceCellOnceCell::get_or_initを使って初期化の処理を関数側から指定することができます。これによってsafeなコードでGLOBAL_DATAを実行時に異なる型(正確にはバリアント)で初期化することができました。

ローカル変数を遅延評価する

Rust のイテレータが遅延評価されることは皆さんもご存知かと思います。これによってイテレータのパフォーマンスや高い表現力が実現されているのでとてもありがたい機能です。
ではイテレータでない変数を遅延評価するにはどうすれば良いでしょうか。実はここでもonce_cell::sync::Lazyが使えます。

use once_cell::sync::Lazy;
let large_data = Lazy::new(|| { ... });

lazy_static::lazy_static!letには対応していないので

lazy_static! {
    let large_data = { ... };
}

とはできません。

and more...

todo!()

まとめ

once_cellを使おう!

zenn に記事を投稿するのは初めてなのでミスや分かりにくい部分もあると思います。コメントでの指摘/補足も歓迎です。

Thanks for your reading!

Discussion

ログインするとコメントできます