RefCell<T> と内部可変性パターン
内部可変性 はRustの設計パターンであり、そのデータへの不変参照が存在している場合でもデータを変更できるようにするものです。通常、この操作は借用規則によって許可されません。このパターンでは、データを変更するために、データ構造の内部で unsafe コードを使って、変更と借用を支配するRustの通常の規則を曲げます。unsafe コードは、コンパイラに対して、それらの規則をコンパイラに任せて検査してもらうのではなく、私たちが手動で検査していることを示します。unsafe コードについては第20章でさらに詳しく説明します。
内部可変性パターンを使う型は、コンパイラには保証できなくても、実行時には借用規則が守られると確実に言える場合にのみ使用できます。そのとき関わる unsafe コードは安全なAPIで包まれており、外側の型自体は依然として不変です。
内部可変性パターンに従う RefCell<T> 型を見ながら、この概念を探っていきましょう。
借用規則を実行時に強制する
Rc<T> とは異なり、RefCell<T> 型は保持しているデータに対する単一所有権を表します。では、RefCell<T> は Box<T> のような型と何が違うのでしょうか? 第4章で学んだ借用規則を思い出してください。
- どの時点でも、持てるのは1つの可変参照か、任意の数の不変参照のどちらかです(両方は不可)。
- 参照は常に有効でなければなりません。
参照や Box<T> では、借用規則の不変条件はコンパイル時に強制されます。RefCell<T> では、これらの不変条件は 実行時に 強制されます。参照では、これらの規則を破るとコンパイラエラーになります。RefCell<T> では、これらの規則を破るとプログラムはパニックして終了します。
コンパイル時に借用規則を検査する利点は、開発プロセスのより早い段階でエラーを検出できることと、解析がすべて事前に完了しているため実行時性能に影響がないことです。そうした理由から、大半の場合においてはコンパイル時に借用規則を検査するのが最善の選択であり、それがRustのデフォルトになっています。
その代わりに実行時に借用規則を検査する利点は、コンパイル時の検査では拒否されていたであろう、特定のメモリ安全なシナリオが許可されるようになることです。Rustコンパイラのような静的解析は、本質的に保守的です。コードを解析するだけでは検出不可能な性質もあります。最も有名な例は停止性問題ですが、これは本書の範囲を超えるため、興味があれば調べてみるとよいでしょう。
一部の解析は不可能であるため、Rustコンパイラはコードが所有権規則に従っていると確信できない場合、正しいプログラムであっても拒否することがあります。そういう意味で、Rustコンパイラは保守的です。もし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>自体が不変であっても、その内部の値を変更できます。
不変値の内部にある値を変更することが、内部可変性パターンです。内部可変性が有用な状況を見て、それがどのように可能になるのかを確認しましょう。
内部可変性を使う
借用規則の帰結として、不変値があるとき、それを可変に借用することはできません。たとえば、次のコードはコンパイルされません。
fn main() {
let x = 5;
let y = &mut x;
}
このコードをコンパイルしようとすると、次のようなエラーになります。
$ cargo run
Compiling borrowing v0.1.0 (file:///projects/borrowing)
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
--> src/main.rs:3:13
|
3 | let y = &mut x;
| ^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
2 | let mut x = 5;
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing` (bin "borrowing") due to 1 previous error
しかし、その値自身のメソッド内では自分を変更できる一方で、他のコードからは不変に見えると便利な場合があります。その値のメソッドの外側にあるコードは、その値を変更できません。RefCell<T> を使うことは内部可変性を実現する方法の1つですが、RefCell<T> が借用規則を完全に回避するわけではありません。コンパイラ内の借用チェッカがこの内部可変性を許可し、その代わり借用規則は実行時に検査されます。規則に違反すると、コンパイラエラーではなく panic! が発生します。
不変値を変更するために RefCell<T> を使える実践的な例を見ながら、なぜそれが有用なのかを確認しましょう。
モックオブジェクトを使ったテスト
テスト中にプログラマは、特定の振る舞いを観察してそれが正しく実装されていることを検証するために、ある型の代わりに別の型を使うことがあります。この代役となる型は テストダブル と呼ばれます。映画制作におけるスタントダブルを思い浮かべてください。特に難しいシーンを演じるために、ある人が俳優の代わりを務めます。テストダブルは、テストを実行しているときに他の型の代わりを務めます。モックオブジェクト は、テスト中に何が起きたかを記録する特定の種類のテストダブルで、正しい操作が行われたことを検証できるようにするものです。
Rustには他の言語と同じ意味でのオブジェクトはなく、また他のいくつかの言語のように標準ライブラリにモックオブジェクト機能が組み込まれているわけでもありません。しかし、モックオブジェクトと同じ目的を果たす構造体を作ることは十分にできます。
これからテストするシナリオは次のとおりです。最大値に対してある値を追跡し、現在の値が最大値にどれくらい近いかに基づいてメッセージを送るライブラリを作成します。たとえばこのライブラリは、ユーザーに許可されたAPI呼び出し回数のクォータを追跡するために使えます。
私たちのライブラリは、値が最大値にどれだけ近いかを追跡し、どのタイミングでどのメッセージを出すべきかという機能だけを提供します。私たちのライブラリを使用するアプリケーションは、メッセージを送信する仕組みを提供することが期待されます。アプリケーションは、そのメッセージを直接ユーザーに表示してもよいですし、メールを送信してもよいですし、テキストメッセージを送信してもよいですし、何か別のことをしてもかまいません。ライブラリはその詳細を知る必要はありません。必要なのは、私たちが提供する Messenger というトレイトを実装した何かだけです。リスト15-20にライブラリのコードを示します。
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, 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 >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
このコードの重要な部分のひとつは、Messenger トレイトが send という 1 つのメソッドを持っており、そのメソッドが self への不変参照とメッセージのテキストを受け取ることです。このトレイトは、モックが実際のオブジェクトと同じように使えるようにするために、私たちのモックオブジェクトが実装する必要のあるインターフェイスです。もうひとつ重要なのは、LimitTracker に対する set_value メソッドの振る舞いをテストしたいという点です。value パラメータに渡すものは変更できますが、set_value はアサーションに使えるようなものを何も返しません。Messenger トレイトを実装した何かと、max に対する特定の値で LimitTracker を作成した場合に、value に異なる数値を渡したとき、適切なメッセージを送るよう messenger に指示されることを確認できるようにしたいのです。
必要なのはモックオブジェクトです。このモックオブジェクトは、send を呼んだときにメールやテキストメッセージを送信する代わりに、送るように指示されたメッセージを追跡するだけにします。モックオブジェクトの新しいインスタンスを作成し、そのモックオブジェクトを使う LimitTracker を作成し、LimitTracker の set_value メソッドを呼び出してから、モックオブジェクトが期待どおりのメッセージを保持していることを確認できます。リスト15-21は、まさにそれを行うモックオブジェクトの実装を試みたものですが、借用チェッカーがこれを許可しません。
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, 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 >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[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);
}
}
このテストコードでは、MockMessenger 構造体を定義しており、その構造体には sent_messages フィールドがあり、そこには送るように指示されたメッセージを追跡するための String 値の Vec が入っています。また、新しい MockMessenger の値を、メッセージの空のリストで開始するように簡単に作成できるよう、関連関数 new も定義しています。次に、MockMessenger に対して Messenger トレイトを実装し、MockMessenger を LimitTracker に渡せるようにしています。send メソッドの定義では、パラメータとして渡されたメッセージを受け取り、それを MockMessenger の sent_messages リストに保存します。
このテストでは、LimitTracker に対して value を max 値の 75 パーセントを超える値に設定するよう指示したときに何が起こるかをテストしています。まず、新しい MockMessenger を作成します。これは空のメッセージリストで開始されます。次に、新しい LimitTracker を作成し、新しい MockMessenger への参照と 100 の max 値を渡します。LimitTracker に対して 80 という値で set_value メソッドを呼び出します。これは 100 の 75 パーセントを超えています。その後、MockMessenger が追跡しているメッセージのリストには 1 件のメッセージが入っているはずだとアサートします。
しかし、ここに示すように、このテストには 1 つ問題があります。
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
--> src/lib.rs:58:13
|
58 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference in the `impl` method and the `trait` definition
|
2 ~ fn send(&mut self, msg: &str);
3 | }
...
56 | impl Messenger for MockMessenger {
57 ~ fn send(&mut self, message: &str) {
|
For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` (lib test) due to 1 previous error
send メソッドは self への不変参照を受け取るため、メッセージを追跡するように MockMessenger を変更できません。また、エラーテキストの提案に従って、impl メソッドとトレイト定義の両方で &mut self を使うこともできません。テストのためだけに Messenger トレイトを変更したくはないからです。代わりに、既存の設計のままでテストコードを正しく動作させる方法を見つける必要があります。
これは、内部可変性が役立つ場面です。sent_messages を RefCell<T> の中に格納すれば、send メソッドは sent_messages を変更して、確認したメッセージを保存できるようになります。リスト15-22にその例を示します。
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, 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 >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[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(80);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
sent_messages フィールドは、今では Vec<String> ではなく RefCell<Vec<String>> 型になっています。new 関数では、空のベクタを包む新しい RefCell<Vec<String>> インスタンスを作成します。
send メソッドの実装では、最初のパラメータは依然として self の不変借用であり、これはトレイト定義と一致しています。self.sent_messages 内の RefCell<Vec<String>> に対して borrow_mut を呼び出し、RefCell<Vec<String>> の内側の値、つまりベクタへの可変参照を取得します。そうすれば、ベクタへの可変参照に対して push を呼び出して、テスト中に送信されたメッセージを追跡できます。
最後に変更しなければならないのはアサーションです。内側のベクタにいくつ要素があるかを確認するために、RefCell<Vec<String>> に対して borrow を呼び出し、ベクタへの不変参照を取得します。
RefCell<T> の使い方を見たところで、それがどのように動作するのかを詳しく見ていきましょう。
実行時に借用を追跡する
不変参照および可変参照を作成するとき、私たちはそれぞれ & および &mut 構文を使います。RefCell<T> では、RefCell<T> に属する安全な API の一部である borrow および borrow_mut メソッドを使います。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> の実装は実行時に panic します。リスト15-23は、リスト15-22にある send の実装を変更したものです。RefCell<T> が実行時にこれを防ぐことを示すため、同じスコープ内で 2 つの可変借用をアクティブにしようと、意図的に試しています。
```rust,ignore,panics
# pub trait Messenger {
# fn send(&self, msg: &str);
# }
#
# pub struct LimitTracker<'a, T: Messenger> {
# messenger: &'a T,
# value: usize,
# max: usize,
# }
#
# impl<'a, T> LimitTracker<'a, T>
# where
# T: Messenger,
# {
# pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, 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 >= 1.0 {
# self.messenger.send("Error: You are over your quota!");
# } else if percentage_of_max >= 0.9 {
# self.messenger
# .send("Urgent warning: You've used up over 90% of your quota!");
# } else if percentage_of_max >= 0.75 {
# self.messenger
# .send("Warning: You've used up over 75% of your quota!");
# }
# }
# }
#
# #[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) {
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));
}
}
#
# #[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.borrow().len(), 1);
# }
# }
borrow_mut から返される RefMut<T> スマートポインタのために、one_borrow という変数を作成します。次に、同じ方法でもう1つの可変借用を two_borrow という変数に作成します。これにより、同じスコープ内に2つの可変参照ができてしまい、これは許可されていません。ライブラリのテストを実行すると、リスト 15-23 のコードはエラーなしでコンパイルされますが、テストは失敗します。
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde)
running 1 test
test tests::it_sends_an_over_75_percent_warning_message ... FAILED
failures:
---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at src/lib.rs:60:53:
RefCell already borrowed
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::it_sends_an_over_75_percent_warning_message
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
コードが already borrowed: BorrowMutError というメッセージでパニックしたことに注目してください。これが、RefCell<T> が実行時に借用ルール違反を処理する方法です。
ここで行ったように、借用エラーをコンパイル時ではなく実行時に検出することを選ぶと、開発プロセスのより後の段階でコード中のミスを見つける可能性があります。場合によっては、コードを本番環境にデプロイするまで見つからないこともあります。また、コンパイル時ではなく実行時に借用を追跡する結果として、コードにはわずかな実行時性能上のペナルティも生じます。しかし RefCell<T> を使うと、不変値しか許可されない文脈で使いながら、受け取ったメッセージを追跡するために自分自身を変更できるモックオブジェクトを書くことが可能になります。こうしたトレードオフはあるものの、通常の参照が提供する以上の機能を得るために RefCell<T> を使うことができます。
可変データに複数の所有者を持たせる
RefCell<T> の一般的な使い方は、Rc<T> と組み合わせることです。Rc<T> を使うと、あるデータに複数の所有者を持たせることができますが、そのデータへのアクセスは不変アクセスだけであることを思い出してください。RefCell<T> を保持する Rc<T> があれば、複数の所有者を持てる うえに 変更もできる値を得られます。
たとえば、リスト 15-18 の cons リストの例では、Rc<T> を使って複数のリストが別のリストの所有権を共有できるようにしていたことを思い出してください。Rc<T> は不変値しか保持しないため、一度作成した後にリスト内の値を変更することはできません。ここで、リスト内の値を変更する能力のために RefCell<T> を追加してみましょう。リスト 15-24 は、Cons の定義で RefCell<T> を使うことで、すべてのリストに格納されている値を変更できることを示しています。
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;
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(3)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));
*value.borrow_mut() += 10;
println!("a after = {a:?}");
println!("b after = {b:?}");
println!("c after = {c:?}");
}
Rc<RefCell<i32>> のインスタンスである値を作成し、それを value という変数に格納して、後で直接アクセスできるようにします。次に、value を保持する Cons バリアントを持つ List を a に作成します。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 を持っていることがわかります。
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.63s
Running `target/debug/cons-list`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))
このテクニックはかなり巧妙です! RefCell<T> を使うことで、外見上は不変の List 値を持つことができます。しかし、内部可変性へのアクセスを提供する RefCell<T> のメソッドを使えば、必要なときにデータを変更できます。借用ルールの実行時チェックによって、データ競合から保護されます。この柔軟性のために、データ構造において少しの速度を犠牲にする価値があることもあります。なお、RefCell<T> はマルチスレッドのコードでは機能しません。Mutex<T> は RefCell<T> のスレッドセーフ版であり、Mutex<T> については第16章で説明します。