lazy_static はもう古い!? once_cell を使おう

2020/09/25に公開

この記事を 3 行でまとめると

  • Rust のグローバル変数には多くの制限があった
  • 制限を撤廃し、容易にグローバル変数を使うためのクレートが lazy_static
  • lazy_static の代替となる once_cell が登場

Rust のグローバル変数には多くの制限があった

Rust にはグローバル変数がありますが、

  • 定数でしか初期化できない
  • 変更可能にすると unsafe を使用する必要がある
  • 変更不可にするとスレッドセーフな型しか使用できない

という制限があり、非常に使いづらい物となっていました。

例えば、一度だけ巨大なテキストを読み込んでグロバール変数に格納したいとします。

しかし、次のように書くと unsafe 無しではグローバル変数を変更できないため、コンパイルエラーとなってしまいます。

static mut LARGE_TEXT : String = String::new();
fn main() {
  LARGE_TEXT = load_large_text();
}

/// 巨大なテキストを読み込む
fn load_large_text() -> String {
  todo!()
}
error[E0133]: use of mutable static is unsafe and requires unsafe function or block
 --> src\main.rs:3:3
  |
3 |   LARGE_TEXT = load_large_text();
  |   ^^^^^^^^^^ use of mutable static
  |
  = note: mutable statics can be mutated by multiple threads: aliasing violations or data races will cause undefined behavior

unsafe を使えばコンパイルエラーは無くなりますが、巨大テキストの使用中にうっかりテキストを書き換えてしまう可能性があり、危険です。

static mut LARGE_TEXT: String = String::new();
fn main() {
    unsafe {
        // 今LARGE_TEXTを使用していないとは限らない、その事をどうか思い出して頂きたい
        LARGE_TEXT = load_large_text();
    }
}

では RefCell を使用すれば実行時に使用中かどうかのチェックが入るため、安全になるかと思えば、RefCell はスレッドセーフではないので使えません。

use std::cell::RefCell;
static LARGE_TEXT: RefCell<String> = RefCell::new(String::new());
fn main() {
    *LARGE_TEXT.borrow_mut() = load_large_text();
}
error[E0277]: `std::cell::RefCell<std::string::String>` cannot be shared between threads safely
 --> src\main.rs:2:1
  |
2 | static LARGE_TEXT: RefCell<String> = RefCell::new(String::new());
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `std::cell::RefCell<std::string::String>` cannot be shared between threads safely
  |
  = help: the trait `std::marker::Sync` is not implemented for `std::cell::RefCell<std::string::String>`
  = note: shared static variables must have a type that implements `Sync`

それなら RefCell のスレッドセーフ版である Mutex ならば使えるかと言えば、グローバル変数は定数以外で初期化できない為、使えません。

use std::sync::Mutex;
static LARGE_TEXT: Mutex<String> = Mutex::new(String::new());
fn main() {
    *LARGE_TEXT.lock().unwrap() = load_large_text();
}
error[E0015]: calls in statics are limited to constant functions, tuple structs and tuple variants
 --> src\main.rs:2:36
  |
2 | static LARGE_TEXT: Mutex<String> = Mutex::new(String::new());
  |

「Rust のグローバル変数って使えない!」

そう思った貴方、正解です。
グローバル変数をそのまま使うのは諦めて、より使いやすくするためのクレートを使いましょう。

制限を撤廃し、容易にグローバル変数を使うためのクレートが lazy_static

lazy_static を使うと、初回アクセス時に一回だけ初期化処理が実行されるグローバル変数を作る事ができます。

use lazy_static::lazy_static;
lazy_static! {
    static ref LARGE_TEXT: String = load_large_text();
}
fn main() {
    println!("{}", *LARGE_TEXT);
}

lazy_static は便利で、あっという間にデファクトスタンダードの地位に上り詰めました。

しかし、この lazy_static、マクロを使っているため微妙にわかりにくいと思いませんか?

lazy_static の代替となる once_cell が登場

そんな中、lazy_static と同様の処理をマクロ無しで実現した once_cell が登場しました。

先ほどの lazy_static の例は once_cell を使用すると次のように書くことができます。

use once_cell::sync::Lazy;
static LARGE_TEXT: Lazy<String> = Lazy::new(|| load_large_text());
fn main() {
    println!("{}", *LARGE_TEXT);
}

マクロを使っていないため、この変数を関数に渡したり構造体のメンバにする、といった応用も可能となっています。

fn may_use_large_text(large_text: &Lazy<String>) {
  todo!()
}

struct LargeTextCache {
    large_text: Lazy<String>,
}

ダウンロード数はまだ lazy_static には及ばないものの急成長しており、標準ライブラリ入りも検討されています。

lazy_static
https://github.com/rust-lang-nursery/lazy-static.rs

once_cell
https://github.com/matklad/once_cell

Discussion