#rustlang における構造体のmutabilityと`Cell/RefCell`
この記事はRust 0.10を基準に書かれている
前提
Rustの基本として、明示的にmut
を付けて値の変更を可能にした(mutableにした)データ以外は変更することができない(デフォルトimmutableの原則とでも呼ぶべきかな)。これを構造体に適用した場合、構造体のフィールドのmutabilityは、フィールドを保持する構造体のそれを引き継ぐ。つまり、親のmutabilityを子は引き継ぐという原則がある。
なので、こういう感じのコード(疑似コードです)はコンパイルエラーになる。
let bar = Bar { bar: Hoge { hoge: 0 }, barbar: 0, }; // `bar`はimmutable bar.barbar = 1; // `bar.hoge`は`bar`のmutabilityを継承するので変更できない bar.hoge.hoge = 1;
まあ詳しくはチュートリアル読んでください。mut
まわりのセマンティクスはそのうち変わるかもしれないし(そういう議論がある)。
この厳格なmutabilityの継承規則は、コードを触る側からすれば安心感がある。メソッドの第一引数で&mut self
などとしない限り、そのメソッドの空間内では、絶対にデータ構造の子孫方向の変更ができないからだ。コードが長期的ないし大人数で保守されているほど、この規則はありがたい。
とはいえ、欠点も存在している。それは、引数Aをmutableな状態で呼び出さなければならないメソッドを、引数Aをimmutableに受け取っているメソッドから呼び出せないということ。具体的にはこんな感じで、self
がらみで頻発し易い。
fn foo(&self) { self.bar(); //selfはimmutableなのでコンパイルエラーになる } fn bar (&mut self) { … } /// 逆なら問題ない fn bar_2(&mut self) { self.hoge_2(); } fn hoge_2(&self) { …. }
unsafeにして、cast::transmute
などで強制的にmutableに属性変更するという手口もあるが、それはRustの目指す方向性に反するので、何度も繰り出していい技ではない。
とはいえ、呼び出しのネストが3世代くらい先のメソッドだけがmutableに引数を受けなければいけないのに、不必要なものまでmutableに受けるというのは気持ち悪い。デフォルトimmutableの恩恵が得られない。ましてや孫にあたるフィールドを変更したいだけなのに、こんなことをしたくはない。超地獄っぽい。
std::cell::{Cell, RefCell}
を用いた解決
というわけで、このmutabilityの連鎖を断ち切るために、Rustには標準ライブラリstd::cell::{Cell, RefCell}
を用いた抜け道が用意されている。
これらのデータ型を用いることにより、親のmutabilityを気にせずに中身を変更できる領域を作ることができる。どうやってこの構造を実現しているのかは簡単なんでコードを読んでくださいな。
Cell
もRefCell
もできることに大差はないのだけれども、名前の通り、前者はPod型(Plain old data)を入れるのに使い、後者は構造体などのコピーに時間がかかるデータを入れるのに使う。
// Cell<T> struct Hoge { hoge: Cell<int>, bar: 0, } let hoge = Hoge { hoge: Cell::new(0), bar: 0 }; let hoge_hoge = hoge.hoge.get(); // 中身はPODなので値のコピー hoge.hoge.set(1); // `hoge`はimmutableだけど、`Cell.set()`を使って値の変更が出来る
struct Fuga { fuga: RefCell<Hoge> } let fuga = Fuga { fuga: RefCell::new(Hoge { …// 初期化 }); }; let fuga_fuga = fuga.fuga.borrow(); //基本的にborrowして中身にアクセス fuga.fuga.borrow_mut().bar = 1; //mutable borrowなので変更できる let copy_fuga_fuga =fuga.fuga.get(); //RefCell<T>が`Clone`を実装してれば、中の値をclone()して取得する.
このようにすることで、mutability chainを断ち切ることができるので、ネストのために不必要なmutable受けを消すことができるようになり、人は幸せになれる。
Servoでの適用事例
色々あるんですが、trickyな前提の上でこれを使っているので、説明がややこしい。ので、気が向いたら(上手く説明できる自身がついたら)そのうち書く。