![]()
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_cellcrateにはいるのでそちらを使っている人も多いと思います。
まとめ
-
OnceCellは一回だけ初期化されるということを利用して不変参照でも初期化できるよ。 -
staticと相性がいいよ。 - 今回安定化されない
LazyCellも便利だよ。