once_cell
がRust 1.70で安定化されます!
というわけで今回はそのonce_cellとstaticについて述べていこうと思います。
once_cellとは?
そもそもonce_cellとは何かというと「一度しか初期化できない」型を提供してくれます。今回安定化されるのは以下の三つの型です。
2番目は1番目のre-exportだけなため、実質二つの型であり、また1番目と3番目の違いは同期版と非同期版のため実質的な違いはありません。
これらはもともとonce_cell
というcrateにいたものです。
OnceCell<T>
の目的としては、「不変参照だけど初期化が一度だけ行われることを保証すること」です。
Rustは不変参照では、「不変」の名の通り、変更を加えることはできません。ただ設定などを使いまわす場合、可変参照だと取り回しが悪いことがあります。また初期化することを遅延させたいことがあります。そういった時に使うことを目的としています。
OnceCell
の中身
内部はどうなっているでしょうか。OnceCell<T>
の場合、中身は単純なUnsafeCell<Option<T>>
になっています。(OnceLock<T>
はLockのため多少複雑になっています。)
OnceCell
にはset
メソッドがあり、ここで初期化をすることができます。
この関数では内部で初期化されているかを調べて、されていなかったら初期化するという単純な処理をしています。
これだけのことにこんな大げさにwrapしないといけないのは、Rustが不変参照での変更を認めていないことで静的な保証をしているからです。OnceCell
はこの不変性の破壊を内部でwrapしてくれます。不変性の破壊が許されるのは、一度しか初期化されないと定義して、それ以外はErr
で返すようにしているからです。
OnceCell
の使い道
static
の制限
さて、Rustにおいて設定などの情報を使い回すとき、主に
- 一番上の関数で初期化して、呼び出す関数に伝播させて参照や
Arc
として持ちまわす。 - staticに入れてしまう。
の二通りがあると思います。
このうち二番目の方法でよくぶつかるのがstaticの制限になります。
Rustのstatic
には他の言語とくらべ制限が多いです。
まず可変なstatic mut
はunsafeです。それはRustがスコープで所有権や可変性などを解析するうえでグローバルスコープなstatic
はどこで変更されるかを保証できないからです。
また定数で初期化する必要があります。
これはstatic
の変数は入れ物であって、例えば動的に値を入れるとなったときどのタイミングで初期化の処理が走るかを制御する必要がでるのですが、それを言語的に一つに決めるのはできないからだと思われます。なので静的(コンパイル時)に値が決まるようにしてあります。
static
とOnceCell
先ほどの制限で定数で初期化することが難しいのは初期化のタイミングの制御を言語的には決めれないからでした。なのでユーザーが決めるためには最初に場所だけ確保して後で値を入れてあげればいいです。つまりOnceCell
を使えばいいのです。
初期化タイミングをユーザーが好きなタイミングでset
してやればいいのです。
更にOnceCell::set
は不変参照でも初期化するようにしてあるのでstatic
の制限を受けなくて済みます。
LazyCell
ユーザーがわざわざ初期化タイミングを指定する必要がなくて、最初にアクセスしたときに初期化されていればいい場合があります。その時にはstd::cell::LazyCell
があります。これは内部的にOnceCell
を抱えて、値がなかったら握っている関数で勝手に初期化するという仕組みです。ただこれは1.70.0では安定化されないです…。デザイン上の問題としてgenericsの型をどうするかという問題があるのでそれさえ解消されれば安定化されるはずでしょう。
この型はonce_cell
crateにはいるのでそちらを使っている人も多いと思います。
まとめ
-
OnceCell
は一回だけ初期化されるということを利用して不変参照でも初期化できるよ。 -
static
と相性がいいよ。 - 今回安定化されない
LazyCell
も便利だよ。