RefCell<T>
と内部可変性パターン
内部可変性は、そのデータへの不変参照がある時でさえもデータを可変化できるRustでのデザインパターンです:
普通、この行動は借用規則により許可されません。データを可変化するために、このパターンは、データ構造内でunsafe
コードを使用して、
可変性と借用を支配するRustの通常の規則を捻じ曲げています。まだ、unsafeコードについては講義していません;
第19章で行います。たとえ、コンパイラが保証できなくても、借用規則に実行時に従うことが保証できる時、
内部可変性パターンを使用した型を使用できます。関係するunsafe
コードはそうしたら、安全なAPIにラップされ、
外側の型は、それでも不変です。
内部可変性パターンに従うRefCell<T>
型を眺めてこの概念を探究しましょう。
RefCell<T>
で実行時に借用規則を強制する
Rc<T>
と異なり、RefCell<T>
型は、保持するデータに対して単独の所有権を表します。では、
どうしてRefCell<T>
がBox<T>
のような型と異なるのでしょうか?第4章で学んだ借用規則を思い出してください:
- いかなる時も(以下の両方ではなく、)1つの可変参照かいくつもの不変参照のどちらかが可能になる
- 参照は常に有効でなければならない。
参照とBox<T>
では、借用規則の不変条件は、コンパイル時に強制されています。RefCell<T>
では、
これらの不変条件は、実行時に強制されます。参照でこれらの規則を破ったら、コンパイルエラーになりました。
RefCell<T>
でこれらの規則を破ったら、プログラムはパニックし、終了します。
コンパイル時に借用規則を精査することの利点は、エラーが開発過程の早い段階で捕捉されることと、 あらかじめ全ての分析が終わるので、実行パフォーマンスへの影響がないことです。それらの理由により、 多くの場合でコンパイル時に借用規則を精査することが最善の選択肢であり、これがRustの既定になっているのです。
借用規則を実行時に代わりに精査する利点は、コンパイル時の精査では許容されない特定のメモリ安全な筋書きが許容されることです。 Rustコンパイラのような静的解析は、本質的に保守的です。コードの特性には、コードを解析するだけでは検知できないものもあります: 最も有名な例は停止性問題であり、この本の範疇を超えていますが、調べると面白い話題です。
不可能な分析もあるので、Rustのコンパイラが、コードが所有権規則に応じていると確証を得られない場合、
正しいプログラムを拒否する可能性があります; このように、保守的なのです。コンパイラが不正なプログラムを受け入れたら、
ユーザは、コンパイラが行う保証を信じることはできなくなるでしょう。しかしながら、
コンパイラが正当なプログラムを拒否するのなら、プログラマは不便に思うでしょうが、悲劇的なことは何も起こり得ません。
コードが借用規則に従っているとプログラマは確証を得ているが、コンパイラがそれを理解し保証することができない時に
RefCell<T>
型は有用です。
Rc<T>
と類似して、RefCell<T>
もシングルスレッドの筋書きで使用するためのものであり、
試しにマルチスレッドの文脈で使ってみようとすると、コンパイルエラーを出します。
RefCell<T>
の機能をマルチスレッドのプログラムで得る方法については、第16章で語ります。
こちらにBox<T>
, Rc<T>
, RefCell<T>
を選択する理由を要約しておきます:
Rc<T>
は、同じデータに複数の所有者を持たせてくれる;Box<T>
とRefCell<T>
は単独の所有者。Box<T>
では、不変借用も可変借用もコンパイル時に精査できる;Rc<T>
では不変借用のみがコンパイル時に精査できる;RefCell<T>
では、不変借用も可変借用も実行時に精査される。RefCell<T>
は実行時に精査される可変借用を許可するので、RefCell<T>
が不変でも、RefCell<T>
内の値を可変化できる。
不変な値の中の値を可変化することは、内部可変性パターンです。内部可変性が有用になる場面を見て、 それが可能になる方法を調査しましょう。
内部可変性: 不変値への可変借用
借用規則の結果は、不変値がある時、可変で借用することはできないということです。 例えば、このコードはコンパイルできません:
fn main() {
let x = 5;
let y = &mut x;
}
このコードをコンパイルしようとしたら、以下のようなエラーが出るでしょう:
error[E0596]: cannot borrow immutable local variable `x` as mutable
(エラー: 不変なローカル変数`x`を可変で借用することはできません)
--> src/main.rs:3:18
|
2 | let x = 5;
| - consider changing this to `mut x`
3 | let y = &mut x;
| ^ cannot borrow mutably
ですが、メソッド内で値が自身を可変化するけれども、他のコードにとっては、
不変に見えることが有用な場面もあります。その値のメソッドの外のコードは、その値を可変化することはできないでしょう。
RefCell<T>
を使うことは、内部可変性を取得する能力を得る1つの方法です。しかし、
RefCell<T>
は借用規則を完全に回避するものではありません: コンパイラの借用チェッカーは、内部可変性を許可し、
借用規則は代わりに実行時に精査されます。この規則を侵害したら、コンパイルエラーではなくpanic!
になるでしょう。
RefCell<T>
を使用して不変値を可変化する実践的な例に取り組み、それが役に立つ理由を確認しましょう。
内部可変性のユースケース: モックオブジェクト
テストダブルは、テスト中に別の型の代わりに使用される型の一般的なプログラミングの概念です。 モックオブジェクトは、テスト中に起きることを記録するテストダブルの特定の型なので、 正しい動作が起きたことをアサートできます。
編注
: テストダブルとは、ソフトウェアテストにおいて、テスト対象が依存しているコンポーネントを置き換える代用品のこと。
Rustには、他の言語でいうオブジェクトは存在せず、また、他の言語のように標準ライブラリにモックオブジェクトの機能が組み込まれてもいません。 ですが、同じ目的をモックオブジェクトとして提供する構造体を作成することは確実にできます。
以下が、テストを行う筋書きです: 値を最大値に対して追跡し、現在値がどれくらい最大値に近いかに基づいてメッセージを送信するライブラリを作成します。 このライブラリは、ユーザが行うことのできるAPIコールの数の割り当てを追跡するのに使用することができるでしょう。
作成するライブラリは、値がどれくらい最大に近いかと、いつどんなメッセージになるべきかを追いかける機能を提供するだけです。
このライブラリを使用するアプリケーションは、メッセージを送信する機構を提供すると期待されるでしょう:
アプリケーションは、アプリケーションにメッセージを置いたり、メールを送ったり、テキストメッセージを送るなどできるでしょう。
ライブラリはその詳細を知る必要はありません。必要なのは、提供するMessenger
と呼ばれるトレイトを実装している何かなのです。
リスト15-20は、ライブラリのコードを示しています:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { pub trait Messenger { fn send(&self, msg: &str); } pub struct LimitTracker<'a, T: 'a + Messenger> { messenger: &'a T, value: usize, max: usize, } impl<'a, T> LimitTracker<'a, T> where T: Messenger { pub fn new(messenger: &T, max: usize) -> LimitTracker<T> { LimitTracker { messenger, value: 0, max, } } pub fn set_value(&mut self, value: usize) { self.value = value; let percentage_of_max = self.value as f64 / self.max as f64; if percentage_of_max >= 0.75 && percentage_of_max < 0.9 { // 警告: 割り当ての75%以上を使用してしまいました self.messenger.send("Warning: You've used up over 75% of your quota!"); } else if percentage_of_max >= 0.9 && percentage_of_max < 1.0 { // 切迫した警告: 割り当ての90%以上を使用してしまいました self.messenger.send("Urgent warning: You've used up over 90% of your quota!"); } else if percentage_of_max >= 1.0 { // エラー: 割り当てを超えています self.messenger.send("Error: You are over your quota!"); } } } }
このコードの重要な部分の1つは、Messenger
トレイトには、self
への不変参照とメッセージのテキストを取るsend
というメソッドが1つあることです。
これが、モックオブジェクトが持つ必要のあるインターフェイスなのです。もう1つの重要な部分は、
LimitTracker
のset_value
メソッドの振る舞いをテストしたいということです。value
引数に渡すものを変えることができますが、
set_value
はアサートを行えるものは何も返してくれません。LimitTracker
をMessenger
トレイトを実装する何かと、
max
の特定の値で生成したら、value
に異なる数値を渡した時にメッセンジャーは適切なメッセージを送ると指示されると言えるようになりたいです。
send
を呼び出す時にメールやテキストメッセージを送る代わりに送ると指示されたメッセージを追跡するだけのモックオブジェクトが必要です。
モックオブジェクトの新規インスタンスを生成し、モックオブジェクトを使用するLimitTracker
を生成し、
LimitTracker
のset_value
を呼び出し、それからモックオブジェクトに期待しているメッセージがあることを確認できます。
リスト15-21は、それだけをするモックオブジェクトを実装しようとするところを示しますが、借用チェッカーが許可してくれません:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; struct MockMessenger { sent_messages: Vec<String>, } impl MockMessenger { fn new() -> MockMessenger { MockMessenger { sent_messages: vec![] } } } impl Messenger for MockMessenger { fn send(&self, message: &str) { self.sent_messages.push(String::from(message)); } } #[test] fn it_sends_an_over_75_percent_warning_message() { let mock_messenger = MockMessenger::new(); let mut limit_tracker = LimitTracker::new(&mock_messenger, 100); limit_tracker.set_value(80); assert_eq!(mock_messenger.sent_messages.len(), 1); } } }
このテストコードはString
のVec
で送信すると指示されたメッセージを追跡するsent_messages
フィールドのあるMockMessenger
構造体を定義しています。
また、空のメッセージリストから始まる新しいMockMessenger
値を作るのを便利にしてくれる関連関数のnew
も定義しています。
それからMockMessenger
にMessenger
トレイトを実装しているので、LimitTracker
にMockMessenger
を与えられます。
send
メソッドの定義で引数として渡されたメッセージを取り、sent_messages
のMockMessenger
リストに格納しています。
テストでは、max
値の75%以上になる何かにvalue
をセットしろとLimitTracker
が指示される時に起きることをテストしています。
まず、新しいMockMessenger
を生成し、空のメッセージリストから始まります。そして、
新しいLimitTracker
を生成し、新しいMockMessenger
の参照と100というmax
値を与えます。
LimitTracker
のset_value
メソッドは80という値で呼び出し、これは100の75%を上回っています。
そして、MockMessenger
が追いかけているメッセージのリストが、今は1つのメッセージを含んでいるはずとアサートします。
ところが、以下のようにこのテストには1つ問題があります:
error[E0596]: cannot borrow immutable field `self.sent_messages` as mutable
(エラー: 不変なフィールド`self.sent_messages`を可変で借用できません)
--> src/lib.rs:52:13
|
51 | fn send(&self, message: &str) {
| ----- use `&mut self` here to make mutable
52 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^ cannot mutably borrow immutable field
send
メソッドはself
への不変参照を取るので、MockMessenger
を変更してメッセージを追跡できないのです。
代わりに&mut self
を使用するというエラーテキストからの提言を選ぶこともできないのです。
そうしたら、send
のシグニチャが、Messenger
トレイト定義のシグニチャと一致しなくなるからです(気軽に試してエラーメッセージを確認してください)。
これは、内部可変性が役に立つ場面なのです!sent_messages
をRefCell<T>
内部に格納し、
そうしたらsend
メッセージは、sent_messages
を変更して見かけたメッセージを格納できるようになるでしょう。
リスト15-22は、それがどんな感じかを示しています:
ファイル名: src/lib.rs
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; use std::cell::RefCell; struct MockMessenger { sent_messages: RefCell<Vec<String>>, } impl MockMessenger { fn new() -> MockMessenger { MockMessenger { sent_messages: RefCell::new(vec![]) } } } impl Messenger for MockMessenger { fn send(&self, message: &str) { self.sent_messages.borrow_mut().push(String::from(message)); } } #[test] fn it_sends_an_over_75_percent_warning_message() { // --snip-- let mock_messenger = MockMessenger::new(); let mut limit_tracker = LimitTracker::new(&mock_messenger, 100); limit_tracker.set_value(75); assert_eq!(mock_messenger.sent_messages.borrow().len(), 1); } } }
さて、sent_messages
フィールドは、Vec<String>
ではなく、型RefCell<Vec<String>>
になりました。
new
関数で、空のベクタの周りにRefCell<Vec<String>>
を新しく作成しています。
send
メソッドの実装については、最初の引数はそれでもself
への不変借用で、トレイト定義と合致しています。
RefCell<Vec<String>>
のborrow_mut
をself.sent_messages
に呼び出し、
RefCell<Vec<String>>
の中の値への可変参照を得て、これはベクタになります。
それからベクタへの可変参照にpush
を呼び出して、テスト中に送られるメッセージを追跡しています。
行わなければならない最後の変更は、アサート内部にあります: 内部のベクタにある要素の数を確認するため、
RefCell<Vec<String>>
にborrow
を呼び出し、ベクタへの不変参照を得ています。
RefCell<T>
の使用法を見かけたので、動作の仕方を深掘りしましょう!
RefCell<T>
で実行時に借用を追いかける
不変および可変参照を作成する時、それぞれ&
と&mut
記法を使用します。RefCell<T>
では、
borrow
とborrow_mut
メソッドを使用し、これらはRefCell<T>
に所属する安全なAPIの一部です。
borrow
メソッドは、スマートポインタ型のRef<T>
を返し、borrow_mut
はスマートポインタ型のRefMut<T>
を返します。
どちらの型もDeref
を実装しているので、普通の参照のように扱うことができます。
RefCell<T>
は、現在活動中のRef<T>
とRefMut<T>
スマートポインタの数を追いかけます。
borrow
を呼び出す度に、RefCell<T>
は活動中の不変参照の数を増やします。Ref<T>
の値がスコープを抜けたら、
不変参照の数は1下がります。コンパイル時の借用規則と全く同じように、RefCell<T>
はいかなる時も、
複数の不変借用または1つの可変借用を持たせてくれるのです。
これらの規則を侵害しようとすれば、参照のようにコンパイルエラーになるのではなく、
RefCell<T>
の実装は実行時にパニックするでしょう。リスト15-23は、リスト15-22のsend
実装に対する変更を示しています。
同じスコープで2つの可変借用が活動するようわざと生成し、RefCell<T>
が実行時にこれをすることを阻止してくれるところを説明しています。
ファイル名: src/lib.rs
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
let mut one_borrow = self.sent_messages.borrow_mut();
let mut two_borrow = self.sent_messages.borrow_mut();
one_borrow.push(String::from(message));
two_borrow.push(String::from(message));
}
}
borrow_mut
から返ってきたRefMut<T>
スマートポインタに対して変数one_borrow
を生成しています。
そして、同様にして変数two_borrow
にも別の可変借用を生成しています。これにより同じスコープで2つの可変参照ができ、
これは許可されないことです。このテストを自分のライブラリ用に走らせると、リスト15-23のコードはエラーなくコンパイルできますが、
テストは失敗するでしょう:
---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at
'already borrowed: BorrowMutError', src/libcore/result.rs:906:4
(スレッド'tests::it_sends_an_over_75_percent_warning_message'は、
'すでに借用されています: BorrowMutError', src/libcore/result.rs:906:4でパニックしました)
note: Run with `RUST_BACKTRACE=1` for a backtrace.
コードは、already borrowed: BorrowMutError
というメッセージとともにパニックしたことに注目してください。
このようにしてRefCell<T>
は実行時に借用規則の侵害を扱うのです。
コンパイル時ではなく実行時に借用エラーをキャッチするということは、開発過程の遅い段階でコードのミスを発見し、
コードをプロダクションにデプロイする時まで発見しない可能性もあることを意味します。また、
コンパイル時ではなく、実行時に借用を追いかける結果として、少し実行時にパフォーマンスを犠牲にするでしょう。
しかしながら、RefCell<T>
を使うことで、不変値のみが許可される文脈で使用しつつ、
自身を変更して見かけたメッセージを追跡するモックオブジェクトを書くことが可能になります。
代償はありますが、RefCell<T>
を使用すれば、普通の参照よりも多くの機能を得ることができるわけです。
Rc<T>
とRefCell<T>
を組み合わせることで可変なデータに複数の所有者を持たせる
RefCell<T>
の一般的な使用法は、Rc<T>
と組み合わせることにあります。Rc<T>
は何らかのデータに複数の所有者を持たせてくれるけれども、
そのデータに不変のアクセスしかさせてくれないことを思い出してください。RefCell<T>
を抱えるRc<T>
があれば、
複数の所有者を持ちそして、可変化できる値を得ることができるのです。
例を挙げれば、Rc<T>
を使用して複数のリストに別のリストの所有権を共有させたリスト15-18のコンスリストの例を思い出してください。
Rc<T>
は不変値だけを抱えるので、一旦生成したら、リストの値はどれも変更できません。RefCell<T>
を含めて、
リストの値を変更する能力を得ましょう。RefCell<T>
をCons
定義で使用することで、
リスト全てに格納されている値を変更できることをリスト15-24は示しています:
ファイル名: src/main.rs
#[derive(Debug)] enum List { Cons(Rc<RefCell<i32>>, Rc<List>), Nil, } use List::{Cons, Nil}; use std::rc::Rc; use std::cell::RefCell; fn main() { let value = Rc::new(RefCell::new(5)); let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil))); let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a)); let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a)); *value.borrow_mut() += 10; println!("a after = {:?}", a); println!("b after = {:?}", b); println!("c after = {:?}", c); }
Rc<RefCell<i32>>
のインスタンスの値を生成し、value
という名前の変数に格納しているので、
直接後ほどアクセスすることができます。そして、a
にvalue
を持つCons
列挙子でList
を生成しています。
value
からa
に所有権を移したり、a
がvalue
から借用するのではなく、a
とvalue
どちらにも中の5
の値の所有権を持たせるよう、
value
をクローンする必要があります。
リストa
をRc<T>
に包んでいるので、リストb
とc
を生成する時に、どちらもa
を参照できます。
リスト15-18ではそうしていました。
a
、b
、c
のリストを作成した後、value
の値に10を足しています。これをvalue
のborrow_mut
を呼び出すことで行い、
これは、第5章で議論した自動参照外し機能(「->
演算子はどこに行ったの?」節をご覧ください)を使用して、
Rc<T>
を内部のRefCell<T>
値に参照外ししています。borrow_mut
メソッドは、
RefMut<T>
スマートポインタを返し、それに対して参照外し演算子を使用し、中の値を変更します。
a
、b
、c
を出力すると、全て5ではなく、変更された15という値になっていることがわかります。
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 6 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 10 }, Cons(RefCell { value: 15 }, Nil))
このテクニックは非常に綺麗です!RefCell<T>
を使用することで表面上は不変なList
値を持てます。
しかし、内部可変性へのアクセスを提供するRefCell<T>
のメソッドを使用できるので、必要な時にはデータを変更できます。
借用規則を実行時に精査することでデータ競合を防ぎ、時としてデータ構造でちょっとのスピードを犠牲にこの柔軟性を得るのは価値があります。
標準ライブラリには、Cell<T>
などの内部可変性を提供する他の型もあり、この型は、内部値への参照を与える代わりに、
値はCell<T>
の内部や外部へコピーされる点を除き似ています。またMutex<T>
もあり、
これはスレッド間で使用するのが安全な内部可変性を提供します; 第16章でその使いみちについて議論しましょう。
これらの型の違いをより詳しく知るには、標準ライブラリのドキュメンテーションをチェックしてください。