0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Rust the book 要約 15章:スマートポインタ

Posted at

目次

15.0 概要

  • スマートポインタは、ポインタのように振る舞うだけでなく、追加のメタデータと能力があるデータ構造
    • スマートポインタは、参照とは異なり、指しているデータを所有する
    • 普通、構造体を使用して実装されている
    • スマートポインタは通常の構造体と異なり、DerefDrop トレイトを実装している
      • Deref:参照外し演算子 * を作用させたときの動作を定義する
      • Drop:値がスコープを抜けるときの動作を定義する
    • 既出の例:String, Vec<T>
    • この章で取り扱う新しい例:
      • Box<T>: ヒープに値を確保する
      • Rc<T>: 複数の所有権を可能にする参照カウント型
      • Ref<T>, RefMut<T>: RefCell<T> を通してアクセスされ、コンパイル時ではなく実行時に借用規則を強制する型
        • 内部可変性パターン: 不変な型が、内部の値を変更するための API を公開する(RefCell<T> の使用例)
        • 循環参照:Rust でもメモリリークの可能性がある(Rc<T>RefCell<T> を組合わせると循環参照が発生しうる)
    • その他、この章では扱わない例として、Cell<T>Mutex<T> が挙げられる(標準ライブラリのドキュメンテー
      ションをチェックせよ)

各参照を選択する理由

  • Rc<T> は、同じデータに複数の所有者を持たせてくれる
    • 一方、Box<T>RefCell<T>単独の所有者
  • Box<T> では、不変借用も可変借用もコンパイル時に精査できる
    • Rc<T> では不変借用のみがコンパイル時に精査できる
    • RefCell<T> では、不変借用も可変借用も実行時に精査される
  • RefCell<T>実行時に精査される可変借用を許可する(不変借用も作成可能)
    • RefCell<T> が不変でも、RefCell<T> 内の値を可変化できる

その他の重要な概念

  • 参照外し型強制

15.1 Box<T>: ヒープのデータを指す

  • Box<T> により、スタックではなくヒープにデータを格納できる
    • スタックにはヒープデータへのポインタが作成される
  • 使用する場面:
    • コンパイル時にはサイズを知ることができない型があり、正確なサイズを要求する文脈でその型の値を使用する時
    • 多くのデータがあり、その所有権を移したいが、その際にデータがコピーされないようにしたい時(パフォーマンスが向上する)
    • 値を所有する必要があり、特定の型であることではなく、特定のトレイトを実装する型であることのみ気にかけている時(トレイトオブジェクト

Box<T> を使ってヒープにデータを格納する

  • Box::new メソッドでボックスを生成できる
  • ボックスは、スコープを抜けるとき、メモリから解放される
    • メモリの解放は、以下の二つに対して起きる:

      • (スタックに格納されている)ボックス
      • (ヒープに格納されている)データ
    • 例:ボックスを使用してヒープに i32 の値を格納する

      • 5 はヒープに確保される
      • main 関数の終わりで b は解放される
      fn main() {
          let b = Box::new(5);
          println!("b = {}", b);  // b = 5
      }
      

ボックスで再帰的な型を可能にする

  • 再帰的な型 はコンパイル時にサイズがわからないので、ボックスを利用して実装する必要がある

再帰的なデータ型の例:コンスリスト

コンスリストとは
  • コンスリストの各要素は、二つの要素を含む:
    • 現在の要素の値
    • 次の要素
  • リストの最後の要素は Nil と呼ばれる値だけを含む(null, nil とは異なるので注意)
問題のある定義
  • コンスリストの enum 定義(この時点では List 型のサイズが不明なのでコンパイル不可):

    enum List {
      Cons(i32, List),
      Nil
    }
    
    • 使用例:

      use List::{Cons, Nil};
      
      fn main() {
          let list = Cons(1, Cons(2, Cons(3, Nil)));
      }
      
Box<T> を用いて既知のサイズの再帰的な型を得る
  • 間接参照(値の代わりに値へのポインタを格納する)を利用して再帰的な型を実現する

  • 使用例:

    • List 列挙子は1つの i32 のサイズに加えてボックスのポインタデータを格納する領域を必要とする(がそれ以上は必要としない)
      • --> コンパイラは List 値を格納するのに必要なサイズを計算できる
    enum List {
      Cons(i32, Box<List>),
      Nil
    }
    
    use List::{Cons, Nil};
    
    fn main() {
      let list = Cons(1, 
          Box::new(Cons(2,
              Box::new(Cons(3,
                  Box::new(Nil)
              ))
          ))
      );
    }
    

15.2 Deref トレイトでスマートポインタを普通の参照のように扱う

  • Deref トレイトを実装することで、参照外し演算子の * の振る舞いをカスタマイズできる
  • 参照外し型強制で参照やスマートポインタをうまく使うことができる

通常の参照に参照外し演算子を適用して値までポインタを追いかける

  • 例:i32 値への参照を生成してから参照外し演算子を使ってデータまで参照を辿る

    fn main() {
      let x = 5;
      let y = &x;
    
      assert_eq!(x, 5);
      assert_eq!(*y, 5):
    }
    

Box<T> に参照外し演算子を適用して値までポインタを追いかける

  • 例:i32 値への参照を生成してから参照外し演算子を使ってデータまで参照を辿る

    fn main() {
      let x = 5;
      let y = Box::new(x);
    
      assert_eq!(x, 5);
      assert_eq!(*y, 5):
    }
    

独自のスマートポインタを定義する(Box<T> に似たスマートポインタの定義)

  • Box<T> 型を模倣するために一要素のタプル構造を定義する:

    struct MyBox<T> (T);
    
    impl<T> MyBox<T> {
        fn new(x: T) -> MyBox<T> {
            MyBox<T>(x)
        }
    }
    
  • MyBox<T> の参照外しの方法を指定していないので、以下のコードはコンパイルできない:

    fn main() {
        let x = 5;
        let y = MyBox::new(x);
    
        assert_eq!(x, 5);
        assert_eq!(*y, 5);
    }
    

Deref トレイトを実装して型を参照のように扱う

  • * 演算子で参照外しできるようにするには、Deref トレイトを実装する必要がある

    • Deref トレイトは deref という 1 つのメソッドの実装を要求する
      • derefself を借用し、内部のデータへの参照を返すメソッド
    • また、type Target = Hoge;Deref トレイトが使用する関連型を定義する(参照外しの結果得られる値の型を定義する?)
    • 例:
    use std::ops::Deref;
    
    struct MyBox<T> (T);
    
    impl<T> Deref for MyBox<T> {
        type target = T;
    
        fn deref(&self) -> &T {
            &self.0
        }
    }
    
  • Deref トレイトを実装している型の変数に参照外しを行うと Rust は水面下で +(hoge.deref()) のようなコードを実行する

    • (Rust が * 演算子を deref メソッドの呼び出しと普通の参照外しへと置き換えてくれる)
    • (普通の参照か Deref を実装した型であるかどうかに関わらず、等しく機能するコードを書ける)
  • * 演算子が deref メソッドの呼び出しと * 演算子の呼び出しに置き換えられるのは、コード内で * を打つ毎にただ 1 回だけ

    • * 演算子の置き換えは無限に繰り返されない)

関数やメソッドで暗黙的な参照外し型強制

  • 参照外し型強制Deref を実装する型への参照が自動で型変換される

  • 例:Deref トレイトを実装している MyBox 型のインスタンスへの参照を &str 型の引数を期待する関数に渡すと型強制が行われる:

    • ここでは、MyBox<T> 型に実装された deref メソッドによって &MyBox<String>&String に変換され、String 型に実装された deref メソッドによって、&String 型が &str に変換される
    // hello 関数は &str 型の引数を期待する
    fn hello(name: &str) {
        println!("Hello, {}!", name);
    }
    
    fn main() {
        let m = MyBox::new(String::from("Rust"));
        hello(&m);  // &str を期待する関数に &MyBox<String> を渡すと参照外し型強制される
        
        // このような参照外し型強制が Rust の機能として実装されていなければ、以下のようなコードを書く必要がある(この機能によりコードがかなり簡素化されている):
        // hello(&(*m)[..]);
    }
    

参照外し型強制を可変性と組合わせる

  • DerefMut トレイトを実装することで、可変参照に * 演算子を作用させたときに可変参照を得る動作を定義できる

    • Deref トレイトを実装することで、不変参照に * 演算子を作用させたときに参照を定義できることに対応)
  • コンパイラは以下の三つの場合に参照外し型強制を実行する:

    1. T: Deref<Target=U> の時、&T から &U
    2. T: DerefMut<Target=U> の時、&mut T から &mut U
    3. T: Deref<Target=U> の時、&mut T から &U
    1. &T があり、T が何らかの型 U への Deref を実装しているなら、透過的に &U を得られる

    2. &mut T があり、T が何らかの型 U への DerefMut を実装しているなら、透過的に &mut U を得られる

    3. &mut T があり、T が何らかの型 U への Deref を実装しているなら、透過的に &U を得られる

    ※不変参照を参照外し型強制で可変参照に変えることはできない

15.3 Drop トレイトで片付け時にコードを走らせる

  • どんな型に対しても Drop トレイトの実装を提供できる

    • スコープを抜けそうになった時に起こることをカスタマイズできる
  • Drop トレイトを実装するには、self への可変参照を取る drop という 1 つのメソッドを実装する必要がある

    • drop 関数には、自分の型のインスタンスがスコープを抜ける時に走らせたいロジックを実装する
  • 例:

    struct CustomSmartPointer {
        data: String,
    }
    
    impl Drop for CustomSmartPointer {
        fn drop(&mut self) {
            println!("Dropping CustomSmartPointer with data `{}`!", self.data);
        }
    }
    
    fn main() {
        let c = CustomSmartPointer {
            data: String::from("my stuff"),
        };
        let d = CustomSmartPointer {
            data: String::from("other stuff"),
        };
        println!("CustomSmartPointers created.");
    } // ここで "Dropping CustomSmartPointer with data `other stuff`!" と "Dropping CustomSmartPointer with data `my stuff`!" が標準出力に表示される
    // 変数は、生成されたのと逆の順序でドロップされるので、`d` は `c` より先にドロップされる
    

std::mem::drop で早期に値をドロップする

  • Rust は、Drop トレイトの drop メソッドを手動で呼ばせてくれない

  • スコープが終わる前に値を強制的にドロップさせたいなら、代わりに標準ライブラリが提供する std::mem::drop 関数を呼ぶ

    • (この関数は初期化処理に含まれている)
  • 例:

    fn main() {
        let c = CustomSmartPointer { data: String::from("some data") };
        println!("CustomSmartPointer created.");
        drop(c);
        println!("CustomSmartPointer dropped before the end of main.");
    }
    
    • このコードを実行すると以下のように出力される(Drop トレイトで定義した drop メソッドが drop(c); の部分で実行される):

      CustomSmartPointer created.
      Dropping CustomSmartPointer with data `some data`!
      CustomSmartPointer dropped before the end of main.
      

15.4 Rc<T> は、参照カウント方式のスマートポインタ

  • 単独の値が複数の所有者を持つ場合もある

    • 例:グラフデータ構造では、複数の辺が同じノードを指す可能性がある。概念的にそのノードはそれを指す全ての辺に所有されるので、指す辺がなくならない限り、ノードは片付けられるべきではない。
  • Rc<T> という型(reference counting(参照カウント)の略)によって、複数の所有権が可能になる

  • Rc<T> 型は、値への参照の数を追跡する

    • これにより値がまだ使用中かどうか決定される
    • 値への参照が 0 なら、どの参照も無効にすることなく、値は片付けられる
  • ヒープにプログラムの複数箇所で読む何らかのデータを確保したいが、コンパイル時にはどの部分が最後にデータを使用し終わるか決定できない時に Rc<T> 型を使用する

  • Rc<T> は、シングルスレッドの筋書きで使用するためだけのもの

Rc<T> の使い方の例

  • Rc<T> の使い方:

    • use 文を追加して Rc<T> をスコープに導入
    • let hoge = Rc::new(fuga)fuga への参照カウンタを作成し、
    • Rc::clone(&hoge)hoge のクローンを作成すると参照カウンタのカウンタ値が+1される
  • 例:510 を含むコンスリスト a を作る。さらに、3 で始まる b4 で始まる c のふたつのコンスリストを作る。 bc のどちらも a リストに接続する

    • b, c のコンスリストはどちらも同じコンスリスト a を共有する)

    コンスリストの共有

  • 以下のような単純な実装はコンパイルエラーを吐く:

    enum List {
        Cons(i32, Box<List>),
        Nil,
    }
    
    use crate::List::{Cons, Nil};
    
    fn main() {
        let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
        let b = Cons(3, Box::new(a));  // コンスリスト a のヒープデータの所有権は,この行で b にムーブ済みなので
        let c = Cons(4, Box::new(a));  // ここではもう a を呼び出せない
    }
    
  • そこで、Box<T> の代わりに Rc<T> を使用することでコンスリストの各要素が複数の所有者を持てるように変更する:

    • 新しい所有者を生み出す際には、Rc::clone を実行して値をクローンする(これを実行する度に、参照カウンタの値が1増える)
    • 初期化処理に含まれていないので、use 文を追加して Rc<T> をスコープに導入する
    enum List {
        Cons(i32, Rc<List>),
        Nil,
    }
    
    use crate::List::{Cons, Nil};
    use std::rc::Rc;
    
    fn main() {
        let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
        let b = Cons(3, Rc::clone(&a));
        let c = Cons(4, Rc::clone(&a));
    }
    

参照カウントが変化することを確認する(Rc::strong::count 関数)

  • Rc::strong::count 関数で参照カウントのカウント値を取得できる

  • 例:

    enum List {
        Cons(i32, Rc<List>),
        Nil,
    }
    
    use crate::List::{Cons, Nil};
    use std::rc::Rc;
    
    fn main() {
        let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
        println!("count after creating a = {}", Rc::strong_count(&a)); // count after creating a = 1
    
        let b = Cons(3, Rc::clone(&a));
        println!("count after creating b = {}", Rc::strong_count(&a)); // count after creating b = 2
        {
            let c = Cons(4, Rc::clone(&a));
            println!("count after creating c = {}", Rc::strong_count(&a)); // count after creating c = 3
    
        }
        println!("count after c goes out of scope = {}", Rc::strong_count(&a)); // count after c goes out of scope = 2
    
    }
    

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

  • 内部可変性:そのデータへの不変参照がある時でさえもデータを可変化できるデザインパターン
    • unsafe を用いて実装されている)

RefCell<T> で実行時に借用規則を強制する

  • RefCell<T> 型は、保持するデータに対して単独の所有権を表す
  • 以下の借用規則 a, b の取り扱いが通常の参照と異なる:
    • 通常の参照 &hogeBox<T> は以下のようなルールがコンパイル時に強制される(破るとコンパイルエラーになる)
      1. 参照は、一つの可変参照か複数の不変参照
      2. 参照は常に有効でなければならない
    • 一方、RefCell<T> では上記の借用規則は実行時に強制される(破ると実行時にパニックを起こす
  • RefCell<T> インスタンスに対して以下のメソッドを呼び出すことでスマートポインタを生成できる:
    • borrow メソッド:Ref<T> を返す(≈ 不変参照)
    • borrow_mut メソッド:RefMut<T> を返す(≈ 可変参照)
  • cf. 通常の参照:
    • &hoge:不変参照
    • &mut hoge:可変参照
  • どちらの参照も Deref トレイトを実装しているので、普通の参照のように扱える

  • RefCell<T>Ref<T> 型あるいは RefMut<T> 型のスマートポインタの数を数える:

    • borrow メソッド(borrow_mut メソッド)を呼び出すたびに不変参照(可変参照)のカウント値を増やす
    • Ref<T>RefMut<T>)の値がスコープを抜けるたびに不変参照(可変参照)のカウント値を減らす
    • 実行時に、借用規則(参照は、一つの可変参照か複数の不変参照)が破られるとパニックを起こす
      • 例:可変参照を複数生成するとパニックが起こる(コンパイルできる)

            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));
                }
            }
        
  • コードが借用規則に従っているとプログラマは確証を得ているが、コンパイラがそれを理解し保証することができない時に RefCell<T> 型は有用

  • RefCell<T> もシングルスレッドの筋書きで使用するためのもの(マルチスレッドの文脈で使ってみようとすると、コンパイルエラーを出す)

内部可変性のユースケース:モックオブジェクト(RefCell<T> の使用例)

  • テストダブル:ソフトウェアテストにおいて、テスト対象が依存しているコンポーネントを置き換える代用品のこと

  • モックオブジェクト:テスト中に起きることを記録するテストダブルの特定の型

  • 考えるテストの筋書き:

    • 値を最大値に対して追跡し、現在値がどれくらい最大値に近いかに基づいてメッセージを送信するライブラリを作成

    • ライブラリのコードは以下のような感じ:

      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 トレイトには、self への不変参照とメッセージのテキストを取る send というメソッドが 1 つある
    • LimitTrackerset_value メソッドの振る舞いをテストしたい
      • しかし、set_value メソッドは assert できるものを返してくれない...
  • --> LimitTrackerset_value メソッドをテストするために、Messenger トレイトを実装するモックオブジェクトを作成して、そのモックオブジェクトが期待通りのメッセージをもつことを確認することでテストを実行したい...

  • --> 以下のような実装を考える(しかし、これは借用チェッカーに拒否される):

    • ここで、もともと、LimitTrackermessenger が不変借用される想定であり、send メソッドが self への不変参照をとることを考えると、mock_messenger が内部に Vec<String> を持ち、そこにメッセージを蓄えるという設計であることは借用規則に反している:
    #[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);
        }
    }
    
  • --> この問題は内部可変性を使えば解消できる:

    #[cfg(test)]
    mod tests {
        use super::*;
    +   use std::cell::RefCell;
    
        struct MockMessenger {
    -       sent_messages: Vec<String>,
    +       sent_messages: RefCell<Vec<String>>,
        }
    
        impl MockMessenger {
            fn new() -> MockMessenger {
                MockMessenger {
    -               sent_messages: vec![],
    +               sent_messages: RefCell::new(vec![]),
                }
            }
        }
    
        impl Messenger for MockMessenger {
            fn send(&self, message: &str) {
    -           self.sent_messages.push(String::from(message));
    +           self.sent_messages.borrow_mut().push(String::from(message));  // borrow_mut() で可変参照を一時的に得て値を push する
            }
        }
    
        #[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.len(), 1);
    +       assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);  //  borrow() で不変参照を得る
        }
    }
    

Rc<T>RefCell<T> を組み合わせることで可変なデータに複数の所有者を持たせる

  • Rc<T>:何らかのデータに複数の所有者を作成できるが、不変のアクセスしかさせてくれない

  • RefCell<T> を抱える Rc<T>Rc<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));  // Rc で包むことで `RefCell { value: 5 }` の所有権を複数生成できるようにする. RefCell で包むことで `5` の不変・可変参照に関する借用規則は実行時にチェックされる
    
        let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));  // a にも 'value の Rc の中身'の所有権を共有
    
        let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));  // b にも 'a の Rc の中身'の所有権を共有
        let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));  // c にも 'a の Rc の中身'の所有権を共有
    
        *value.borrow_mut() += 10;  //  value の borrow_mut メソッドを呼び出すことで自動参照外しして Rc を剥きながら、RefMut (RefCell の中身への可変参照)に変換し、この可変参照に対して `*` を適用することで value の中身 `5` を可変的に扱っている
    
        println!("a after = {:?}", a);  // a after = Cons(RefCell { value: 15 }, Nil)
        println!("b after = {:?}", b);  // b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
        println!("c after = {:?}", c);  // c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))
    }
    

15.6 循環参照は、メモリをリークすることもある

  • Rc<T>RefCell<T> を組合わせると循環参照によって参照のカウント値が0にならず、メモリリークを起こす例を構築できる
  • すなわち、Rust ではメモリリークを完全に回避することはできない
  • Rc<T> 値を含む RefCell<T> 値があるなどの内部可変性と参照カウントのある型がネストして組み合わさっていたら、循環していないことをプログラマの側で保証しなければならない!(コンパイラは保証してくれない...)

循環参照の例

  • 例:循環参照が起こる例

    use crate::List::{Cons, Nil};
    use std::cell::RefCell;
    use std::rc::Rc;
    
    #[derive(Debug)]
    enum List {
        Cons(i32, RefCell<Rc<List>>),
        Nil,
    }
    
    impl List {
        fn tail(&self) -> Option<&RefCell<Rc<List>>> {
            match self {
                Cons(_, item) => Some(item),
                Nil => None,
            }
        }
    }
    
    fn main() {
        let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
    
        println!("a initial rc count = {}", Rc::strong_count(&a));  // 1
        println!("a next item = {:?}", a.tail());  // Some(RefCell { value: Nil })
    
        let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
    
        println!("a rc count after b creation = {}", Rc::strong_count(&a));  // 2
        println!("b initial rc count = {}", Rc::strong_count(&b));  // 1
        println!("b next item = {:?}", b.tail());  // Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
    
        if let Some(link) = a.tail() {
            *link.borrow_mut() = Rc::clone(&b);  // ここで a のしっぽを b に向けさせて循環を作成する
        }
    
        println!("b rc count after changing a = {}", Rc::strong_count(&b));  // 2
        println!("a rc count after changing a = {}", Rc::strong_count(&a));  // 2
    
        // Uncomment the next line to see that we have a cycle;
        // it will overflow the stack
        // println!("a next item = {:?}", a.tail());
    }
    
    • 生成される循環参照を表す図:
      循環参照

循環参照を回避する: Rc<T>Weak<T> に変換する

  • Rc::clone を呼び出して、Rc<T> インスタンスの strong_count を +1 する代わりに、

  • Rc:downgrade を呼び出して、Rc<T> インスタンスの weak_count を +1することができる(Weak<T> 型のスマートポインタが得られる)

    • strong_count が 0 になると、Rc<T> インスタンスが片づけられるが、 weak_count が 0 になってもとくに何も起こらない
  • Weak<T> が参照する値はドロップされてしまっている可能性がある

    • Weak<T>upgrade メソッドを呼び出すことで Option<Rc<T>> を取得する

要するに

  • Rc::downgradeRc::clone の代わり.
    • weak_count を +1 する.
    • Weak<T> 型のスマートポインタを生成
  • Weak<T>upgrade メソッド:参照外し兼 Option の unwrap

Weak<T> の使用例:木データ構造を作る(子ノードのある Node)

  • Node 構造体を定義:
    • value:各ノードの値
    • children:ノードに続くノードの配列
use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        children: RefCell::new(vec![]),
    });

    let branch = Rc::new(Node {
        value: 5,
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });
}
子供から親への参照を追加する
  • Node 構造体に parent フィールドを追加する
    • 循環参照を避けるために parent の型は RefCell<Weak<Node>> とする(RefCellparent を可変化するために使用)
    • ただし、親ノードは子ノードを所有すべき(親が消えたら子も消えるべき)なので、children の型は RefCell<Vec<Rc<Node>>> のままでよい
use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),  // `Weak::new` で中身が空の `Weak<T>` スマートポインタを作成
        children: RefCell::new(vec![]),
    });

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());  // leaf parent = None  (この時点では `leaf.parent` の中身は `None`)

    let branch = Rc::new(Node {
        value: 5,
        parent: RefCell::new(Weak::new()),  // `Weak::new` で中身が空の `Weak<T>` スマートポインタを作成
        children: RefCell::new(vec![Rc::clone(&leaf)]),
    });

    // `parent` の `RefCell` の中身(`Weak<Node>` 型)に対して参照外し演算子 `*` を作用させて `parent` の中身に変更を加えている. 
    // `Rc::downgrade` で参照 `&branch` を `Weak<T>` 型のスマートポインタに変換している
    *leaf.parent.borrow_mut() = Rc::downgrade(&branch);  

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());  /* 
      leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
      children: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },
      children: RefCell { value: [] } }] } })
    (この時点では `leaf.parent` の中に `Weak<Rc<Node>>` 型の値(`branch`)が入っている)
    */
}
strong_countweak_count への変更を可視化する
  • strong_countweak_count への変更を観察する
use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,
    children: RefCell<Vec<Rc<Node>>>,
}

fn main() {
    let leaf = Rc::new(Node {
        value: 3,
        parent: RefCell::new(Weak::new()),
        children: RefCell::new(vec![]),
    });

    println!(
        "leaf strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf),
    ); // leaf strong = 1, weak = 0

    {
        let branch = Rc::new(Node {
            value: 5,
            parent: RefCell::new(Weak::new()),
            children: RefCell::new(vec![Rc::clone(&leaf)]), // leaf の strong_count +1
        });

        *leaf.parent.borrow_mut() = Rc::downgrade(&branch); // branch の weak_count +1

        println!(
            "branch strong = {}, weak = {}",
            Rc::strong_count(&branch),
            Rc::weak_count(&branch),
        ); // branch strong = 1, weak = 1

        println!(
            "leaf strong = {}, weak = {}",
            Rc::strong_count(&leaf),
            Rc::weak_count(&leaf),
        ); // leaf strong = 2, weak = 0
    } // ここで `branch` がドロップする(leaf の strong_count が -1)

    println!("leaf parent = {:?}", leaf.parent.borrow().upgrade()); // leaf parent = None
    println!(
        "leaf strong = {}, weak = {}",
        Rc::strong_count(&leaf),
        Rc::weak_count(&leaf),
    ); // leaf strong = 1, weak = 0
}

参考文献

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?