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!");
        }
    }
}
}

リスト15-20: 値が最大値にどれくらい近いかを追跡し、特定のレベルの時に警告するライブラリ

このコードの重要な部分の1つは、Messengerトレイトには、selfへの不変参照とメッセージのテキストを取るsendというメソッドが1つあることです。 これが、モックオブジェクトが持つ必要のあるインターフェイスなのです。もう1つの重要な部分は、 LimitTrackerset_valueメソッドの振る舞いをテストしたいということです。value引数に渡すものを変えることができますが、 set_valueはアサートを行えるものは何も返してくれません。LimitTrackerMessengerトレイトを実装する何かと、 maxの特定の値で生成したら、valueに異なる数値を渡した時にメッセンジャーは適切なメッセージを送ると指示されると言えるようになりたいです。

sendを呼び出す時にメールやテキストメッセージを送る代わりに送ると指示されたメッセージを追跡するだけのモックオブジェクトが必要です。 モックオブジェクトの新規インスタンスを生成し、モックオブジェクトを使用するLimitTrackerを生成し、 LimitTrackerset_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);
    }
}
}

リスト15-21: 借用チェッカーが許可してくれないMockMessengerを実装しようとする

このテストコードはStringVecで送信すると指示されたメッセージを追跡するsent_messagesフィールドのあるMockMessenger構造体を定義しています。 また、空のメッセージリストから始まる新しいMockMessenger値を作るのを便利にしてくれる関連関数のnewも定義しています。 それからMockMessengerMessengerトレイトを実装しているので、LimitTrackerMockMessengerを与えられます。 sendメソッドの定義で引数として渡されたメッセージを取り、sent_messagesMockMessengerリストに格納しています。

テストでは、max値の75%以上になる何かにvalueをセットしろとLimitTrackerが指示される時に起きることをテストしています。 まず、新しいMockMessengerを生成し、空のメッセージリストから始まります。そして、 新しいLimitTrackerを生成し、新しいMockMessengerの参照と100というmax値を与えます。 LimitTrackerset_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_messagesRefCell<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);
    }
}
}

リスト15-22: 外側の値は不変と考えられる一方でRefCell<T>で内部の値を可変化する

さて、sent_messagesフィールドは、Vec<String>ではなく、型RefCell<Vec<String>>になりました。 new関数で、空のベクタの周りにRefCell<Vec<String>>を新しく作成しています。

sendメソッドの実装については、最初の引数はそれでもselfへの不変借用で、トレイト定義と合致しています。 RefCell<Vec<String>>borrow_mutself.sent_messagesに呼び出し、 RefCell<Vec<String>>の中の値への可変参照を得て、これはベクタになります。 それからベクタへの可変参照にpushを呼び出して、テスト中に送られるメッセージを追跡しています。

行わなければならない最後の変更は、アサート内部にあります: 内部のベクタにある要素の数を確認するため、 RefCell<Vec<String>>borrowを呼び出し、ベクタへの不変参照を得ています。

RefCell<T>の使用法を見かけたので、動作の仕方を深掘りしましょう!

RefCell<T>で実行時に借用を追いかける

不変および可変参照を作成する時、それぞれ&&mut記法を使用します。RefCell<T>では、 borrowborrow_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: 同じスコープで2つの可変参照を生成してRefCell<T>がパニックすることを確かめる

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);
}

リスト15-24: Rc<RefCell<i32>>で可変化できるListを生成する

Rc<RefCell<i32>>のインスタンスの値を生成し、valueという名前の変数に格納しているので、 直接後ほどアクセスすることができます。そして、avalueを持つCons列挙子でListを生成しています。 valueからaに所有権を移したり、avalueから借用するのではなく、avalueどちらにも中の5の値の所有権を持たせるよう、 valueをクローンする必要があります。

リストaRc<T>に包んでいるので、リストbcを生成する時に、どちらもaを参照できます。 リスト15-18ではそうしていました。

abcのリストを作成した後、valueの値に10を足しています。これをvalueborrow_mutを呼び出すことで行い、 これは、第5章で議論した自動参照外し機能(「->演算子はどこに行ったの?」節をご覧ください)を使用して、 Rc<T>を内部のRefCell<T>値に参照外ししています。borrow_mutメソッドは、 RefMut<T>スマートポインタを返し、それに対して参照外し演算子を使用し、中の値を変更します。

abcを出力すると、全て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章でその使いみちについて議論しましょう。 これらの型の違いをより詳しく知るには、標準ライブラリのドキュメンテーションをチェックしてください。

関連キーワード:  RefCell, 借用, 可変, 参照, 内部, Rc, リスト, borrow, self, MockMessenger