導入
cellモジュールにはCell, RefCell, UnsafeCellの三種類の構造体が定義されています。それらの違いを内部実装に軽く触れつつ解説したいと思います。
詳しい解説が不要な方は「cellモジュール」と「使い分け」をお読みください。
借用ルール
Rustの借用ルールについて復習しておきましょう。
Rustは参照に関して、可変な参照が唯一つ存在するか、不変な参照が一つ以上存在するかしか認めていません。もしこのルールがなければ以下のようなコードが通ることになります。
let mut v = vec![0, 1, 2];
let s = &v[1..]; // ここでvを借用した
v.pop(); // 既に借用されているvを可変に借用するので本当ならコンパイルエラー
println!("{}", s[1]); // 既にポップされた無効な値への参照を使用としている
このように借用ルールはきれいなプログラムを強制するためだけのものではなく、安全性のために必要不可欠ものであります。
cellモジュール
上述のようにRustの借用ルールは必要なものですが、ある値を複数の場所から書き換えられたほうがいいこともあります。そこで借用ルールを迂回する際にどのような戦略を取るかに応じて、何でもできるがアンセーフなUnsafeCell, 値のみに制限したCell, 参照も扱えるが少しオーバーヘッドのあるRefCellがcellモジュールで定義されています。1
UnsafeCell<T>
UnsafeCell<T>は安全装置の付いていない2cell型です。UnsafeCell<T>型はT型そのものとメモリ上でのレイアウトは同じで、&UnsafeCell<T>のような不変参照がある場合も内部のT型の値が書き換えられ得ることをコンパイラに伝えるだけの型です。
UnsafeCell<T>に対して定義されている重要な関数は次のものです。
pub fn get(&self) -> *mut T;
これは内部の値へのポインタを返す関数であり、このポインタを経由して内部の値を書き換えることができます。
ただし、このポインタを&Tや&mut Tに変換する際には通常通りRustの借用ルールに従っていなければなりません。すなわち可変参照&mut Tが唯一つ存在するか、不変参照&Tのみが一つ以上存在するという状況のいずれかでなければなりません。
このルールを破らないことが使用者に委ねられているという意味でアンセーフなCellです。
Rustから特別扱いされている型はこのUnsafeCellのみで、CellとRefCellはUnsafeCellを用いることで標準ライブラリでなくとも実装することができます。
Cell
これはTを値として扱うためのcell型です。内部的にはUnsafeCell<T>をラップしているだけなのでオーバーヘッドがありません。
この型の特徴は&Cell<T>型から&T型の参照を得る関数が定義されていないことです。これにより内部の値への参照がないことが保証され、pub fn set(&self, val: T)のような値を書き換える関数の内部で一時的に&mut T型の参照を得ても他に&T型の参照が存在しないので借用ルールに反しないことが保証されます。
ここで気をつけなければならないことは、マルチスレッドで値を書き換える関数が同時に呼ばれて可変参照が唯一でなくなってしまう場合ですが、Cellは!Sync, つまり複数のスレッドで共有できないようになっているので、同時に呼び出される可能性は除外されます。
内部の値への参照を得る方法がないということでしたが、では実際内部の値を使いたいときどうするかというと、主に以下の三種類の方法があります。
-
fn into_inner(self) -> T:Cell<T>自体を消費して内部の値を取り出します -
fn replace(&self, val: T) -> T: 内部の値を他の値に置き換えて元の値を取り出します -
fn get_mut(&mut self) -> &mut T: 内部の値へのミュータブルな参照を得ます
get_mutは内部への参照を得る関数ですが、Cell<T>自体をミュータブルに借用して、新たにCell<T>を借用するset等を呼び出せなくすることで整合性を保っています。
RefCell
内部のTへの参照を得ることもできるCellの上位互換です。ただし借用ルールを実行時に検査するので少しのオーバーヘッドがあります。
RefCell<T>はCell<T>の持つ関数に加えて、次の関数をサポートしています。
pub fn borrow(&self) -> Ref<'_, T>;
pub fn borrow_mut(&self) -> RefMut<'_, T>;
返り値が&Tや&mut TではなくRef<T>やRefMut<T>になっているのが気になる方も多いはずなのでその理由を説明します。
RefCellは可変な借用が存在するときにborrowを呼び出したり、借用が存在するときにborrow_mutを呼び出したりするとパニックします。
これを実行時に確認するためにRefCell<T>は借用の個数を管理する追加のフィールドを持っています。
借用が終わった際、自動的にカウントを減らす機能をRef<T>やRefMut<T>のDropとして実装するため、RefやRefMutを返すことになっています。
Ref<T>はDeref3 (RefMut<T>はそれに加えてDerefMut) を実装しているため必要に応じて自動的に&T (RefMut<T>は&mut T) に変換されるので、通常の参照と同様に使うことができます。
使い分け
- まず
Cellを検討しましょう。書き換えたい値への参照が必要ない場合Cellで事足ります。 - 参照が必要な場合は実行時に安全性を検査する
RefCellを使うのが基本的な選択です。例えばプログラム全体で大きな構造体を静的変数に保持する場合などは、その値への参照がほしいことが多いはずなのでRefCellが適しているでしょう。 -
RefCellの実行時のオーバーヘッドが許容できない場合はUnsafeCellを気をつけて使うことになります。
あとがき
これまでなんとなくでCellとRefCellを使っていたのですが、曖昧な理解だったので調べてみました。
同じように雰囲気で使っている人の理解に役立てば幸いです。