Rc<T>
は、参照カウント方式のスマートポインタ
大多数の場合、所有権は明らかです: 一体どの変数が与えられた値を所有しているかわかるのです。 ところが、単独の値が複数の所有者を持つ可能性のある場合もあります。例えば、グラフデータ構造では、 複数の辺が同じノードを指す可能性があり、概念的にそのノードはそれを指す全ての辺に所有されるわけです。 指す辺がなくならない限り、ノードは片付けられるべきではありません。
複数の所有権を可能にするため、RustにはRc<T>
という型があり、これは、reference counting(参照カウント)の省略形です。
Rc<T>
型は、値がまだ使用中かどうか決定する値への参照の数を追跡します。値への参照が0なら、どの参照も無効にすることなく、
値は片付けられます。
Rc<T>
を家族部屋のテレビと想像してください。1人がテレビを見に部屋に入ったら、テレビをつけます。
他の人も部屋に入ってテレビを観ることができます。最後の人が部屋を離れる時、
もう使用されていないので、テレビを消します。他の人がまだ観ているのに誰かがテレビを消したら、
残りのテレビ視聴者が騒ぐでしょう!
ヒープにプログラムの複数箇所で読む何らかのデータを確保したいけれど、
コンパイル時にはどの部分が最後にデータを使用し終わるか決定できない時にRc<T>
型を使用します。
どの部分が最後に終わるかわかっているなら、
単にその部分をデータの所有者にして、コンパイル時に強制される普通の所有権ルールが効果を発揮するでしょう。
Rc<T>
は、シングルスレッドの筋書きで使用するためだけのものであることに注意してください。
第16章で並行性について議論する時に、マルチスレッドプログラムで参照カウントをする方法を講義します。
Rc<T>
でデータを共有する
リスト15-5のコンスリストの例に回帰しましょう。Box<T>
を使って定義したことを思い出してください。
今回は、両方とも3番目のリストの所有権を共有する2つのリストを作成します。
これは概念的には図15-3のような見た目になります:
5と10を含むリストa
を作ります。さらにもう2つリストを作ります: 3で始まるb
と4で始まるc
です。
b
とc
のどちらもそれから5と10を含む最初のa
リストに続きます。換言すれば、
どちらのリストも5と10を含む最初のリストを共有しています。
List
の定義を使用してBox<T>
とともにこの筋書きを実装しようとしても、うまくいきません。
リスト15-17のようにですね:
ファイル名: src/main.rs
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let a = Cons(5,
Box::new(Cons(10,
Box::new(Nil))));
let b = Cons(3, Box::new(a));
let c = Cons(4, Box::new(a));
}
このコードをコンパイルすると、こんなエラーが出ます:
error[E0382]: use of moved value: `a`
--> src/main.rs:13:30
|
12 | let b = Cons(3, Box::new(a));
| - value moved here
13 | let c = Cons(4, Box::new(a));
| ^ value used here after move
|
= note: move occurs because `a` has type `List`, which does not implement
the `Copy` trait
Cons
列挙子は、保持しているデータを所有するので、b
リストを作成する時に、
a
がb
にムーブされ、b
がa
を所有します。それからc
を作る際に再度a
を使用しようとすると、
a
はムーブ済みなので、できないわけです。
Cons
の定義を代わりに参照を保持するように変更することもできますが、そうしたら、
ライフタイム引数を指定しなければなりません。ライフタイム引数を指定することで、
リストの各要素が最低でもリスト全体と同じ期間だけ生きることを指定することになります。
例えば、借用チェッカーはlet a = Cons(10, &Nil);
をコンパイルさせてくれません。
一時的なNil
値が、a
が参照を得られるより前にドロップされてしまうからです。
代わりに、List
の定義をリスト15-18のように、Box<T>
の箇所にRc<T>
を使うように変更します。
これで各Cons
列挙子は、値とList
を指すRc<T>
を保持するようになりました。b
を作る際、
a
の所有権を奪うのではなく、a
が保持しているRc<List>
をクローンします。それによって、
参照の数が1から2に増え、a
とb
にそのRc<List>
にあるデータの所有権を共有させます。
また、c
を生成する際にもa
をクローンするので、参照の数は2から3になります。Rc::clone
を呼ぶ度に、
Rc<List>
内のデータの参照カウントが増え、参照が0にならない限りデータは片付けられません。
ファイル名: src/main.rs
enum List { Cons(i32, Rc<List>), Nil, } use 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)); }
初期化処理に含まれていないので、use
文を追加してRc<T>
をスコープに導入する必要があります。
main
で5と10を保持するリストを作成し、a
の新しいRc<List>
に格納しています。それから、
b
とc
を作成する際に、Rc::clone
関数を呼び出し、引数としてa
のRc<List>
への参照を渡しています。
Rc::clone(&a)
ではなく、a.clone()
を呼ぶこともできますが、Rustのしきたりは、この場合Rc::clone
を使うことです。
Rc::clone
の実装は、多くの型のclone
実装のように、全てのデータのディープコピーをすることではありません。
Rc::clone
の呼び出しは、参照カウントをインクリメントするだけであり、時間はかかりません。
データのディープコピーは時間がかかることもあります。参照カウントにRc::clone
を使うことで、
視覚的にディープコピーをする類のクローンと参照カウントを増やす種類のクローンを区別することができます。
コード内でパフォーマンスの問題を探す際、ディープコピーのクローンだけを考慮し、Rc::clone
の呼び出しを無視できるのです。
Rc<T>
をクローンすると、参照カウントが増える
a
のRc<List>
への参照を作ったりドロップする毎に参照カウントが変化するのが確かめられるように、
リスト15-18の動く例を変更しましょう。
リスト15-19で、リストc
を囲む内側のスコープができるようmain
を変更します;
そうすれば、c
がスコープを抜けるときに参照カウントがどう変化するか確認できます。
ファイル名: src/main.rs
enum List { Cons(i32, Rc<List>), Nil, } use List::{Cons, Nil}; use std::rc::Rc; fn main() { let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); // a生成後のカウント = {} println!("count after creating a = {}", Rc::strong_count(&a)); let b = Cons(3, Rc::clone(&a)); // b生成後のカウント = {} println!("count after creating b = {}", Rc::strong_count(&a)); { let c = Cons(4, Rc::clone(&a)); // c生成後のカウント = {} println!("count after creating c = {}", Rc::strong_count(&a)); } // cがスコープを抜けた後のカウント = {} println!("count after c goes out of scope = {}", Rc::strong_count(&a)); }
プログラム内で参照カウントが変更される度に、参照カウントを出力します。参照カウントは、
Rc::strong_count
関数を呼び出すことで得られます。Rc<T>
型にはweak_count
もあるので、
この関数はcount
ではなくstrong_count
と命名されています; weak_count
の使用目的は、
「循環参照を回避する」節で確かめます。
このコードは、以下の出力をします:
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2
a
のRc<List>
は最初1という参照カウントであることがわかります; そして、clone
を呼び出す度に、
カウントは1ずつ上がります。c
がスコープを抜けると、カウントは1下がります。参照カウントを増やすのに、
Rc::clone
を呼ばなければいけなかったみたいに参照カウントを減らすのに関数を呼び出す必要はありません:
Rc<T>
値がスコープを抜けるときにDrop
トレイトの実装が自動的に参照カウントを減らします。
この例でわからないことは、b
そしてa
が、main
の終端でスコープを抜ける時に、カウントが0になり、
その時点でRc<List>
が完全に片付けられることです。Rc<T>
を使用すると、単独の値に複数の所有者を持たせることができ、
所有者のいずれかが存在している限り、値が有効であり続けることをカウントは保証します。
不変参照経由で、Rc<T>
は読み取り専用にプログラムの複数箇所間でデータを共有させてくれます。
Rc<T>
が複数の可変参照を存在させることも許可してくれたら、第4章で議論した借用ルールの1つを侵害する虞があります:
同じ場所への複数の可変借用は、データ競合や矛盾を引き起こすことがあるのです。しかし、
データを可変化する能力はとても有用です!次の節では、内部可変性パターンと、
Rc<T>
と絡めて使用してこの不変性制限を手がけられるRefCell<T>
型について議論します。