スマートポインタ Rc<T> - 参照カウンタ
Rc<T>, the Reference Counted Smart Pointer
所有権は多くの場合において明白である。つまり「1変数は、1つの値の所有する 」。
しかしながら 一つの値が複数の所有者を持つ ケースが存在する。 例として、グラフ構造がある。 グラフは、複数の辺が同一のノードを指し示しす。つまりノードは概念的にそれを指し示す全ての辺によって所有される。 またノードは、それを指し示す辺がある限り、削除されるべきではない。
Rustは、複数の所有権を可能にするために、Rc<T>
型(参照カウンタ(reference counting)と称される)を持つ。
Rc<T>
型は、値が使われているかどうかを決定するための値への参照の数 をトラックし続ける。
もし値への参照数が0になれば、その時、値は消去される。
Rc<T>
を部屋に置かれたテレビとすれば、一人の人間がそのテレビをつけると、その他の人も部屋に入ってテレビを見ることができ、
最後の人間が離れると、もはや使用されていないので、テレビを消すことができる。
もし誰かが他の人が見ているのに、テレビを消した場合、大論争になるかもしれません。
とあるデータをプログラムのあちこちで読むためにヒープ上に配置したいとき、そしてどの箇所が最後に使用されるのか決定できないときに、Rc<T>
型を使用します。
万が一、もし最後に使用する場所が分かっているならば、その場所をまさに所有者と決定することができるので、コンパイル時間に強制される通常の所有権ルールが効果を発揮するかもしれません。
Note:
Rc<T>
型は1つのスレッド上のみで動作させる。マルチスレッドにおける参照カウントは、こちら。
Rc<T> - データの共有
以下のcons listを考える。b
,c
からのノードは直接a
を指し示すノードを参照している。
これをBox<T>
型で書くと、所有権の問題が発生する。
enum List { Cons(i32, Box<List>), Nil, } fn main() { let a = Cons(5, Box::new(Cons(10, Box::new(Nil)))); let b = Cons(3, Box::new(a)); // aの所有権が移動する let c = Cons(4, Box::new(a)); // エラーが発生する
もしくはこちらのようにa
への参照を直接持つようにすると、ライフタイムを明示する必要が発生する。
enum List<'a> { Cons(i32, &'a List<'a>), Nil, } use crate::List::{Cons, Nil}; // リファレンスでは動かないとあるが、v1.50では動く let a = Cons(5, &Cons(10, &Nil)); let b = Cons(3, &a); let c = Cons(4, &a);
(v1.50以前では、借用チェッカーが、&Cons(10, &Nil)
をコンパイルさせることを許可しない。なぜならば変数a
が一時的なNil
値の参照を取る前にドロップさせてしまうため。)
その代わりに、Box<T>
からRc<T>
型に変更する。
enum List { Cons(i32, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::rc::Rc; let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); // a: Rc<List> let b = Cons(3, Rc::clone(&a)); // Rc::cloneによって、aへの参照カントを増やす let c = Cons(4, Rc::clone(&a)); // Rc::cloneによって、aへの参照カウントを増やす
Cons
は、値(i32)とList
を指し示すRc<T>
を持つ。
変数b
を作るとき、a
の所有権を取る代わりに、a
をホールドするRc<List>
をクローンし、a
への参照カウントを増やす。
毎回Rc::clone
を呼ぶごとに、Rc<List>
を内部に含んでいるデータへの参照カウントは増加すし、その値が0になるまでは、データは消去されない。
Rc<T>
を使うには、use
ステートメントを使ってスコープに追加する必要がある。プレリュード(前奏曲)にないため。
このケースに置いては、Rc::clone(&a)
の代わりに、a.clone()
を呼ぶことができるが、Rustの慣習では、Rc::clone
を使用する。
Rc::clone
は、ディープコピーを作るのではなく、参照カウントをインクリメントするのみである。
Rc::clone
によって、ディープコピーをするクローンと参照カウントを増加するクローンの違いを視覚的にすることができる。
これはコードのパフォーマンスの問題を探す際に、Rc::clone
を呼び出しを無視することができ、ディープコピークローンを考えるだけですむようになる。
Cloning an Rc<T> - 参照カウントのインクリメント
変数aのRc<List>
への参照を作成・ドロップさせることで、参照カウントの変更を見ていく。
enum List { Cons(i32, Rc<List>), Nil, } use crate::List::{Cons, Nil}; // a: Rc<List> let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); println!("count after creating a = {}", Rc::strong_count(&a)); let b = Cons(3, Rc::clone(&a)); println!("count after creating b = {}", Rc::strong_count(&a)); { let c = Cons(4, Rc::clone(&a)); println!("count after creating c = {}", Rc::strong_count(&a)); } println!("count after c goes out of scope = {}", Rc::strong_count(&a)); /* count after creating a = 1 count after creating b = 2 count after creating c = 3 count after c goes out of scope = 2 */
Rc::strong_count
関数を呼ぶことで、参照カウントを取得することができる。
この関数は、count
ではなく、strong_count
と命名されているのは、Rc<T>
型は、weak_count
という関数をも持つためである。
weak_count
については、こちらを参照。
イミュータブル参照を通して、Rc<T>
は、複数のプログラムのあちこちの間で、データへの参照を許可する。
もしRc<T>
が、ミュータブルな参照を許可することがあれば、借用ルールを破壊することになる。
同じデータへの複数のミュータブルな参照は、データ競合と矛盾を引き起こす。
しかし、可変データは非常に有用である。
次のセクションでは、内部の可変パターンとRefCell<T>
について議論する。
RefCell<T>
型は、この不可変な制限をもつRc<T>
とともに使用することができる。
まとめ
Rc<T>
によって
- スマートポインタの一種
- ヒープ上の値への参照を、参照ではなく値(
Rc<T>
型)として保持することができる - イミュータブルな参照を実現
- 参照では必要となるライフタイムの明示を除去