導入
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>
はDeref
3 (RefMut<T>
はそれに加えてDerefMut
) を実装しているため必要に応じて自動的に&T
(RefMut<T>
は&mut T
) に変換されるので、通常の参照と同様に使うことができます。
使い分け
- まず
Cell
を検討しましょう。書き換えたい値への参照が必要ない場合Cell
で事足ります。 - 参照が必要な場合は実行時に安全性を検査する
RefCell
を使うのが基本的な選択です。例えばプログラム全体で大きな構造体を静的変数に保持する場合などは、その値への参照がほしいことが多いはずなのでRefCell
が適しているでしょう。 -
RefCell
の実行時のオーバーヘッドが許容できない場合はUnsafeCell
を気をつけて使うことになります。
あとがき
これまでなんとなくでCell
とRefCell
を使っていたのですが、曖昧な理解だったので調べてみました。
同じように雰囲気で使っている人の理解に役立てば幸いです。