はじめに
Rc
この実験では、Rust における Rc
Rc<T>、参照カウントスマートポインタ
ほとんどの場合、所有権は明確です。つまり、特定の値を所有している変数が誰であるかを正確に知っています。ただし、単一の値に複数の所有者がいる場合もあります。たとえば、グラフデータ構造では、複数のエッジが同じノードを指すことがあり、そのノードは概念的にそれを指すすべてのエッジによって所有されます。ノードに指すエッジがなく、所有者がいない場合に限り、ノードをクリーンアップする必要があります。
Rust の型Rc<T>を使用して明示的に複数の所有権を有効にする必要があります。これは、「参照カウント」の略です。Rc<T>型は、値がまだ使用されているかどうかを判断するために、値への参照数を追跡します。値への参照がゼロの場合、値はクリーンアップされ、参照が無効になることはありません。
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を含む最初のリストを共有します。
Box<T>を使ったListの定義を使ってこのシナリオを実装しようとすると、うまくいきません。15-17 に示すように、コンパイル時にエラーが発生します。
ファイル名:src/main.rs
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))));
1 let b = Cons(3, Box::new(a));
2 let c = Cons(4, Box::new(a));
}
リスト 15-17:Box<T>を使って 2 つのリストが 3 番目のリストの所有権を共有しようとするとエラーになることを示す
このコードをコンパイルすると、次のエラーが表示されます。
error[E0382]: use of moved value: `a`
--> src/main.rs:11:30
|
9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
| - move occurs because `a` has type `List`, which
does not implement the `Copy` trait
10 | let b = Cons(3, Box::new(a));
| - value moved here
11 | let c = Cons(4, Box::new(a));
| ^ value used here after move
Consのバリアントは、それが保持するデータを所有しているため、bのリストを作成するときに[1]、aがbに移動し、bがaを所有するようになります。その後、cを作成するときに再びaを使用しようとすると[2]、aが移動しているため許可されません。
Consの定義を変更して参照を保持するようにすることもできますが、その場合、寿命期間パラメータを指定する必要があります。寿命期間パラメータを指定することで、リストの各要素がリスト全体と同じくらい長く生き続けることを指定することになります。これは 15-17 の要素とリストの場合に当てはまりますが、すべてのシナリオでは当てはまりません。
代わりに、Listの定義を変更して、Box<T>の代わりにRc<T>を使用します。15-18 に示すように、各Consのバリアントは、値とListを指すRc<T>を保持するようになります。bを作成するときに、aの所有権を取得する代わりに、aが保持しているRc<List>をクローンします。これにより、参照数が 1 から 2 に増え、aとbがそのRc<List>内のデータの所有権を共有するようになります。また、cを作成するときにもaをクローンし、参照数を 2 から 3 に増やします。Rc::cloneを呼び出すたびに、Rc<List>内のデータへの参照カウントが増え、参照がゼロにならない限り、データはクリーンアップされません。
ファイル名:src/main.rs
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
1 use std::rc::Rc;
fn main() {
2 let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
3 let b = Cons(3, Rc::clone(&a));
4 let c = Cons(4, Rc::clone(&a));
}
リスト 15-18:Rc<T>を使用するListの定義
Rc<T>をスコープに入れるためにuse文を追加する必要があります[1]。なぜなら、これはプレリュードには含まれていないからです。mainでは、5と10を保持するリストを作成し、aの新しいRc<List>に格納します[2]。その後、b[3]とc[4]を作成するときに、Rc::clone関数を呼び出し、aのRc<List>への参照を引数として渡します。
a.clone()ではなくRc::clone(&a)を呼ぶこともできますが、この場合、Rust の慣例はRc::cloneを使用することです。Rc::cloneの実装は、ほとんどの型のcloneの実装のように、すべてのデータの深いコピーを行いません。Rc::cloneの呼び出しは、参照カウントのみを増やすだけで、それほど時間がかかりません。データの深いコピーには多くの時間がかかる場合があります。参照カウント用にRc::cloneを使用することで、深いコピーのクローンと参照カウントを増やすクローンを視覚的に区別することができます。コードのパフォーマンス問題を探すときには、深いコピーのクローンのみを考慮すればよく、Rc::cloneの呼び出しは無視することができます。
Rc<T>をクローンすると参照カウントが増える
15-18 の動作例を変更して、aのRc<List>への参照を作成および破棄する際に参照カウントがどのように変化するかを見てみましょう。
15-19 では、mainを変更して、リストcの周りに内部スコープを持たせます。そうすることで、cがスコープ外になったときの参照カウントの変化を見ることができます。
ファイル名:src/main.rs
--snip--
fn main() {
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)
);
}
リスト 15-19:参照カウントを表示する
プログラムの各時点で参照カウントが変化するときに、Rc::strong_count関数を呼び出すことで参照カウントを表示します。この関数はcountではなくstrong_countと名付けられています。なぜなら、Rc<T>型にはweak_countもあるからです。「Weak<T>を使った参照サイクルの防止」で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を呼ぶ必要があるのと同じように、参照カウントを減らすために関数を呼ぶ必要はありません。Dropトレイトの実装は、Rc<T>値がスコープ外になるときに自動的に参照カウントを減らします。
この例では見えないことですが、mainの最後でb、そしてaがスコープ外になると、カウントが 0 になり、Rc<List>が完全にクリーンアップされます。Rc<T>を使用することで、単一の値に複数の所有者を持たせることができ、カウントにより、所有者のいずれかがまだ存在する限り、値が有効なままであることが保証されます。
不変参照を介して、Rc<T>は読み取り専用でプログラムの複数の部分間でデータを共有することを可能にします。Rc<T>が複数の可変参照も持てるようになっていた場合、第 4 章で説明した借用規則の 1 つに違反する可能性があります。同じ場所に対する複数の可変借用は、データ競合や不整合を引き起こす可能性があるからです。しかし、データを変更できることは非常に便利です!次のセクションでは、内部可変性パターンと、この不変性制約とともに使用できるRefCell<T>型について説明します。
まとめ
おめでとうございます!Rc