RefCell<T> と内部可変性パターン

RustRustBeginner
今すぐ練習

This tutorial is from open-source community. Access the source code

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

RefCell と内部可変性パターンへようこそ。この実験は、Rust Book の一部です。LabEx で Rust のスキルを磨くことができます。

この実験では、Rust における内部可変性の概念と、RefCell<T> 型を使ってそれがどのように実装されるかを探ります。

RefCell<T>{=html} と内部可変性パターン

内部可変性 は、Rust のデザインパターンであり、データに不変参照がある場合でも、そのデータを変更できるようにするものです。通常、この操作は借用規則によって禁止されます。データを変更するには、このパターンではデータ構造内の unsafe コードを使って、Rust の通常の変更と借用を管理する規則を曲げます。unsafe コードは、コンパイラに対して、コンパイラによるチェックに依存するのではなく、手動で規則をチェックしていることを示します。第19章で、unsafe コードについてもっと詳しく説明します。

コンパイラがそれを保証できない場合でも、実行時に借用規則が守られることを保証できる場合にのみ、内部可変性パターンを使う型を使用できます。その場合、関係する unsafe コードは安全な API にラップされ、外側の型は依然として不変です。

内部可変性パターンに従う RefCell<T> 型を見て、この概念を調べてみましょう。

RefCell<T>{=html} を使って実行時に借用規則を強制する

Rc<T> とは異なり、RefCell<T> 型は保持するデータの単一所有権を表します。では、RefCell<T>Box<T> のような型とどのように異なるのでしょうか。第4章で学んだ借用規則を思い出してみてください。

  • 任意の時点で、可変参照を1つまたは不変参照を複数持つことができます(両方はできません)。
  • 参照は常に有効でなければなりません。

参照と 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つの重要な部分は、LimitTrackerset_value メソッドの動作をテストしたいことです [2]。value パラメータに渡す値を変更することはできますが、set_value はアサーション用に何も返しません。Messenger トレイトを実装するものと max の特定の値を持つ LimitTracker を作成した場合、value に異なる数値を渡すと、メッセンジャーに適切なメッセージを送信するように指示されることを言えるようになりたいです。

send を呼び出したときにメールやテキストメッセージを送信する代わりに、送信されたメッセージを追跡するだけのモックオブジェクトが必要です。モックオブジェクトの新しいインスタンスを作成し、そのモックオブジェクトを使用する LimitTracker を作成し、LimitTrackerset_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]、MockMessengerLimitTracker に渡せるようにします。send メソッドの定義で [5]、パラメータとして渡されたメッセージを受け取り、sent_messagesMockMessenger リストに保存します。

テストでは、LimitTrackervaluemax 値の 75% 以上に設定するように指示したときに何が起こるかをテストしています [6]。まず、空のメッセージリストで始まる新しい MockMessenger を作成します。次に、新しい LimitTracker を作成し、新しい MockMessenger への参照と max 値 100 を渡します。LimitTrackerset_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_messagesRefCell<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_messagesRefCell<Vec<String>> に対して borrow_mut を呼び出して [3]、RefCell<Vec<String>> 内の値(つまりベクトル)に対する可変参照を取得します。その後、ベクトルの可変参照に対して push を呼び出して、テスト中に送信されたメッセージを追跡します。

最後に行う変更は、アサーションです。内部のベクトルに何個の要素があるかを確認するには、RefCell<Vec<String>> に対して borrow を呼び出して、ベクトルに対する不変参照を取得します [4]。

ここで RefCell<T> をどのように使用するかを見てきたので、それがどのように機能するかを掘り下げてみましょう!

RefCell<T>{=html} を使って実行時に借用を追跡する

不変参照と可変参照を作成する際、それぞれ &&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: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> を使用することができます。

Rc<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 バリアントを持つ Lista で作成します [2]。value をクローンする必要があります。これにより、avalue の両方が内部の 5 の値の所有権を持ち、value から a に所有権を移すことも、avalue から借用することもありません。

リスト aRc<T> でラップします。そうすると、リスト bc を作成する際、両方とも a を参照できるようになります。これは、リスト15-18で行ったことと同じです。

abc のリストを作成した後、value の値に 10 を加えたいと思います [3]。これは、valueborrow_mut を呼び出すことで行います。これは、「-> 演算子はどこにあるの?」で説明した自動の参照解除機能を使って、Rc<T> を内部の RefCell<T> 値に参照解除します。borrow_mut メソッドは RefMut<T> スマートポインタを返し、それに参照解除演算子を使用して内部の値を変更します。

abc を出力すると、それぞれが 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 と内部可変性パターンの実験を完了しました。あなたのスキルを向上させるために、LabEx でさらに多くの実験を行うことができます。