はじめに
RefCell
この実験では、Rust における内部可変性の概念と、RefCell<T>
型を使ってそれがどのように実装されるかを探ります。
This tutorial is from open-source community. Access the source code
💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください
RefCell
この実験では、Rust における内部可変性の概念と、RefCell<T>
型を使ってそれがどのように実装されるかを探ります。
<T>
{=html} と内部可変性パターン内部可変性 は、Rust のデザインパターンであり、データに不変参照がある場合でも、そのデータを変更できるようにするものです。通常、この操作は借用規則によって禁止されます。データを変更するには、このパターンではデータ構造内の unsafe
コードを使って、Rust の通常の変更と借用を管理する規則を曲げます。unsafe
コードは、コンパイラに対して、コンパイラによるチェックに依存するのではなく、手動で規則をチェックしていることを示します。第19章で、unsafe
コードについてもっと詳しく説明します。
コンパイラがそれを保証できない場合でも、実行時に借用規則が守られることを保証できる場合にのみ、内部可変性パターンを使う型を使用できます。その場合、関係する unsafe
コードは安全な API にラップされ、外側の型は依然として不変です。
内部可変性パターンに従う RefCell<T>
型を見て、この概念を調べてみましょう。
<T>
{=html} を使って実行時に借用規則を強制するRc<T>
とは異なり、RefCell<T>
型は保持するデータの単一所有権を表します。では、RefCell<T>
が Box<T>
のような型とどのように異なるのでしょうか。第4章で学んだ借用規則を思い出してみてください。
参照と Box<T>
では、借用規則の不変性はコンパイル時に強制されます。RefCell<T>
では、これらの不変性は実行時に強制されます。参照の場合、これらの規則を破るとコンパイラエラーが発生します。RefCell<T>
の場合、これらの規則を破ると、プログラムはパニックになり、終了します。
コンパイル時に借用規則をチェックする利点は、開発プロセスでエラーを早期にキャッチできることであり、すべての分析が事前に完了するため、実行時のパフォーマンスに影響がないことです。これらの理由から、コンパイル時に借用規則をチェックすることは、ほとんどの場合で最善の選択であり、これが Rust のデフォルトである理由です。
代わりに実行時に借用規則をチェックする利点は、コンパイル時のチェックで禁止されていた場合でも、特定のメモリセーフなシナリオが許可されることです。Rust コンパイラのような静的分析は、本質的に保守的です。コードの一部のプロパティは、コードを分析することで検出することができません。最も有名な例は停止問題であり、本書の範囲を超えていますが、研究する興味深いトピックです。
一部の分析が不可能なため、Rust コンパイラがコードが所有権規則に準拠していることを確信できない場合、正しいプログラムを拒否する可能性があります。このように、保守的なのです。Rust が不正なプログラムを受け入れる場合、ユーザーは Rust が提供する保証を信頼できなくなります。ただし、Rust が正しいプログラムを拒否する場合、プログラマーは不便になりますが、何ら災害的なことは起こりません。RefCell<T>
型は、コードが借用規則に従っていることを確信しているが、コンパイラが理解して保証できない場合に便利です。
Rc<T>
と同様に、RefCell<T>
は単一スレッドのシナリオでのみ使用でき、マルチスレッドコンテキストで使用しようとするとコンパイル時エラーが発生します。第16章で、マルチスレッドプログラムで RefCell<T>
の機能を得る方法について説明します。
以下は、Box<T>
、Rc<T>
、または RefCell<T>
を選ぶ理由のまとめです。
Rc<T>
は同じデータの複数の所有者を可能にします。Box<T>
と RefCell<T>
は単一の所有者を持ちます。Box<T>
はコンパイル時にチェックされる不変または可変借用を許可します。Rc<T>
はコンパイル時にチェックされる不変借用のみを許可します。RefCell<T>
は実行時にチェックされる不変または可変借用を許可します。RefCell<T>
は実行時にチェックされる可変借用を許可するため、RefCell<T>
が不変である場合でも、RefCell<T>
内の値を変更できます。不変値の中の値を変更することは、内部可変性 パターンです。内部可変性が役立つ状況を見て、それがどのように可能かを調べてみましょう。
借用規則の結果、不変値を持っている場合、それを可変的に借用することはできません。たとえば、このコードはコンパイルされません。
ファイル名:src/main.rs
fn main() {
let x = 5;
let y = &mut x;
}
このコードをコンパイルしようとすると、次のエラーが表示されます。
error[E0596]: cannot borrow `x` as mutable, as it is not declared
as mutable
--> src/main.rs:3:13
|
2 | let x = 5;
| - help: consider changing this to be mutable: `mut x`
3 | let y = &mut x;
| ^^^^^^ cannot borrow as mutable
ただし、値がそのメソッド内で自身を変更することが役立つ場合でも、他のコードには不変であるように見える状況があります。値のメソッドの外のコードは、値を変更することはできません。RefCell<T>
を使用することは、内部可変性を持つ能力を得るための1つの方法ですが、RefCell<T>
は借用規則を完全に回避するわけではありません。コンパイラの借用チェッカーはこの内部可変性を許可し、借用規則は実行時にチェックされます。規則に違反すると、コンパイラエラーではなく panic!
が発生します。
RefCell<T>
を使って不変値を変更する実際の例を見てみましょう。そして、それがなぜ役立つのかを理解しましょう。
テスト中には、プログラマーが特定の動作を観察し、それが正しく実装されていることをアサートするために、ある型の代わりに別の型を使用することがあります。この置き換え用の型は「テストダブル」と呼ばれます。映画制作におけるスタントダブルのように考えてください。ある人が登場して、特別に難しいシーンを演じる俳優の代わりになります。テストを実行しているとき、テストダブルは他の型に代わって機能します。「モックオブジェクト」は、テスト中に何が起こったかを記録する特定の種類のテストダブルであり、正しいアクションが行われたことをアサートできるようになります。
Rust には、他の言語にあるのと同じ意味でのオブジェクトはありません。また、他の言語のように、標準ライブラリにモックオブジェクト機能が組み込まれていません。ただし、モックオブジェクトと同じ目的を果たす構造体を作成することは確かにできます。
ここでテストするシナリオを見てみましょう。ある値を最大値と比較して追跡し、現在の値が最大値に近い程度に応じてメッセージを送信するライブラリを作成します。たとえば、このライブラリは、ユーザーが許可されている API 呼び出し回数の制限を追跡するために使用できます。
私たちのライブラリは、値が最大値にどれだけ近いかを追跡し、いつどのようなメッセージを送信すべきかを提供する機能のみを提供します。私たちのライブラリを使用するアプリケーションは、メッセージを送信するメカニズムを提供する必要があります。アプリケーションは、アプリケーション内にメッセージを置いたり、メールを送信したり、テキストメッセージを送信したり、その他の何かを行うことができます。ライブラリはその詳細を知る必要はありません。必要なのは、私たちが提供する Messenger
と呼ばれるトレイトを実装するものだけです。リスト15-20にライブラリコードを示します。
ファイル名:src/lib.rs
pub trait Messenger {
1 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,
}
}
2 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: You're at 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You're at 75% of your quota!");
}
}
}
リスト15-20:値が最大値にどれだけ近いかを追跡し、値が特定のレベルに達したときに警告するライブラリ
このコードの重要な部分の1つは、Messenger
トレイトに send
と呼ばれる1つのメソッドがあり、これは self
への不変参照とメッセージのテキストを取ります [1]。このトレイトは、モックオブジェクトが実装する必要があるインターフェイスであり、モックを実際のオブジェクトと同じように使用できるようになります。もう1つの重要な部分は、LimitTracker
の set_value
メソッドの動作をテストしたいことです [2]。value
パラメータに渡す値を変更することはできますが、set_value
はアサーション用に何も返しません。Messenger
トレイトを実装するものと max
の特定の値を持つ LimitTracker
を作成した場合、value
に異なる数値を渡すと、メッセンジャーに適切なメッセージを送信するように指示されることを言えるようになりたいです。
send
を呼び出したときにメールやテキストメッセージを送信する代わりに、送信されたメッセージを追跡するだけのモックオブジェクトが必要です。モックオブジェクトの新しいインスタンスを作成し、そのモックオブジェクトを使用する LimitTracker
を作成し、LimitTracker
の set_value
メソッドを呼び出し、その後、モックオブジェクトに期待されるメッセージがあることを確認します。リスト15-21は、それを行うためのモックオブジェクトの実装の試みを示していますが、借用チェッカーはそれを許可しません。
ファイル名:src/lib.rs
#[cfg(test)]
mod tests {
use super::*;
1 struct MockMessenger {
2 sent_messages: Vec<String>,
}
impl MockMessenger {
3 fn new() -> MockMessenger {
MockMessenger {
sent_messages: vec![],
}
}
}
4 impl Messenger for MockMessenger {
fn send(&self, message: &str) {
5 self.sent_messages.push(String::from(message));
}
}
#[test]
6 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);
}
}
リスト15-21:借用チェッカーに許可されない MockMessenger
の実装の試み
このテストコードは、送信されたメッセージを追跡するための String
値の Vec
を持つ sent_messages
フィールドを持つ MockMessenger
構造体を定義しています [1] [2]。また、空のメッセージリストで始まる新しい MockMessenger
値を作成するのを便利にするために、関連付けられた関数 new
を定義しています [3]。その後、MockMessenger
用に Messenger
トレイトを実装して [4]、MockMessenger
を LimitTracker
に渡せるようにします。send
メソッドの定義で [5]、パラメータとして渡されたメッセージを受け取り、sent_messages
の MockMessenger
リストに保存します。
テストでは、LimitTracker
に value
を max
値の 75% 以上に設定するように指示したときに何が起こるかをテストしています [6]。まず、空のメッセージリストで始まる新しい MockMessenger
を作成します。次に、新しい LimitTracker
を作成し、新しい MockMessenger
への参照と max
値 100 を渡します。LimitTracker
の set_value
メソッドに値 80 を渡して呼び出します。これは 100 の 75% 以上です。その後、MockMessenger
が追跡しているメッセージのリストに現在 1 つのメッセージがあることをアサートします。
ただし、このテストには1つの問題があります。ここに示すように:
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a
`&` reference
--> src/lib.rs:58:13
|
2 | fn send(&self, msg: &str);
| ----- help: consider changing that to be a mutable reference:
`&mut self`
...
58 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a
`&` reference, so the data it refers to cannot be borrowed as mutable
send
メソッドが self
への不変参照を取るため、MockMessenger
を変更してメッセージを追跡することはできません。また、エラーメッセージの提案を受けて &mut self
を代わりに使用しようとしても、send
のシグネチャが Messenger
トレイト定義のシグネチャと一致しなくなるため(試してみて、どのようなエラーメッセージが表示されるか見てみてください)、できません。
これは内部可変性が役立つ状況です!sent_messages
を RefCell<T>
内に保存し、その後 send
メソッドが sent_messages
を変更して見たメッセージを保存できるようにします。リスト15-22にそれがどのように見えるかを示します。
ファイル名:src/lib.rs
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
1 sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
2 sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages
3.borrow_mut()
.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
--snip--
assert_eq!(
4 mock_messenger.sent_messages.borrow().len(),
1
);
}
}
リスト15-22:外部の値が不変と見なされる間に内部の値を変更するために RefCell<T>
を使用する
sent_messages
フィールドは現在、Vec<String>
ではなく RefCell<Vec<String>>
型になっています [1]。new
関数では、空のベクトルの周りに新しい RefCell<Vec<String>>
インスタンスを作成します [2]。
send
メソッドの実装では、最初のパラメータは依然として self
の不変借用であり、これはトレイト定義と一致します。self.sent_messages
の RefCell<Vec<String>>
に対して borrow_mut
を呼び出して [3]、RefCell<Vec<String>>
内の値(つまりベクトル)に対する可変参照を取得します。その後、ベクトルの可変参照に対して push
を呼び出して、テスト中に送信されたメッセージを追跡します。
最後に行う変更は、アサーションです。内部のベクトルに何個の要素があるかを確認するには、RefCell<Vec<String>>
に対して borrow
を呼び出して、ベクトルに対する不変参照を取得します [4]。
ここで RefCell<T>
をどのように使用するかを見てきたので、それがどのように機能するかを掘り下げてみましょう!
<T>
{=html} を使って実行時に借用を追跡する不変参照と可変参照を作成する際、それぞれ &
と &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));
}
}
リスト15-23:RefCell<T>
がパニックになることを確認するために、同じスコープ内に 2 つの可変参照を作成する
borrow_mut
から返される RefMut<T>
スマートポインタ用に変数 one_borrow
を作成します。その後、同じ方法で変数 two_borrow
に別の可変借用を作成します。これにより、同じスコープ内に 2 つの可変参照が作成され、これは許可されていません。ライブラリのテストを実行すると、リスト15-23のコードはエラーなくコンパイルされますが、テストは失敗します。
---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'main' panicked at 'already borrowed: BorrowMutError', src/lib.rs:60:53
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
メッセージ already borrowed: BorrowMutError
でパニックになったことに注意してください。これが、RefCell<T>
が実行時に借用規則の違反を処理する方法です。
ここで行ったように、コンパイル時ではなく実行時に借用エラーをキャッチすることを選ぶと、開発プロセスの後半でコードの間違いを見つける可能性があります。おそらく、コードが本番環境にデプロイされるまでになるかもしれません。また、実行時に借用を追跡する代わりにコンパイル時に追跡することで、コードにはわずかな実行時のパフォーマンスペナルティがかかります。ただし、RefCell<T>
を使用することで、不変値のみが許可されるコンテキストで使用している間、自身を変更して見たメッセージを追跡できるモックオブジェクトを書くことができます。通常の参照が提供する機能よりも多くの機能を得るために、トレードオフがあるにもかかわらず、RefCell<T>
を使用することができます。
<T>
{=html} と RefCell<T>
{=html} を使って可変データの複数の所有者を許可するRefCell<T>
を使用する一般的な方法は、Rc<T>
と組み合わせることです。Rc<T>
は、あるデータの複数の所有者を持つことができますが、そのデータには不変なアクセスのみを提供します。RefCell<T>
を保持する Rc<T>
がある場合、複数の所有者を持ち、かつ変更することができる値を取得することができます!
たとえば、リスト15-18のコンセケンスリストの例を思い出してください。そこでは、Rc<T>
を使って複数のリストが別のリストの所有権を共有できるようにしました。Rc<T>
は不変値のみを保持するため、作成した後はリスト内の値を変更することができません。リスト内の値を変更する能力のために RefCell<T>
を追加してみましょう。リスト15-24は、Cons
の定義で RefCell<T>
を使用することで、すべてのリストに格納されている値を変更できることを示しています。
ファイル名:src/main.rs
#[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() {
1 let value = Rc::new(RefCell::new(5));
2 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));
3 *value.borrow_mut() += 10;
println!("a after = {:?}", a);
println!("b after = {:?}", b);
println!("c after = {:?}", c);
}
リスト15-24:変更可能な List
を作成するために Rc<RefCell<i32>>
を使用する
Rc<RefCell<i32>>
のインスタンスである値を作成し、value
という名前の変数に保存します [1]。これにより、後で直接アクセスできるようになります。その後、value
を保持する Cons
バリアントを持つ List
を a
で作成します [2]。value
をクローンする必要があります。これにより、a
と value
の両方が内部の 5
の値の所有権を持ち、value
から a
に所有権を移すことも、a
が value
から借用することもありません。
リスト a
を Rc<T>
でラップします。そうすると、リスト b
と c
を作成する際、両方とも a
を参照できるようになります。これは、リスト15-18で行ったことと同じです。
a
、b
、c
のリストを作成した後、value
の値に 10 を加えたいと思います [3]。これは、value
の borrow_mut
を呼び出すことで行います。これは、「->
演算子はどこにあるの?」で説明した自動の参照解除機能を使って、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: 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>
のスレッドセーフなバージョンであり、第16章で Mutex<T>
について説明します。
おめでとうございます!あなたは RefCell