はじめに
Rc
この実験では、Rust における Rc
This tutorial is from open-source community. Access the source code
💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください
Rc
この実験では、Rust における Rc
<T>
{=html}、参照カウントスマートポインタほとんどの場合、所有権は明確です。つまり、特定の値を所有している変数が誰であるかを正確に知っています。ただし、単一の値に複数の所有者がいる場合もあります。たとえば、グラフデータ構造では、複数のエッジが同じノードを指すことがあり、そのノードは概念的にそれを指すすべてのエッジによって所有されます。ノードに指すエッジがなく、所有者がいない場合に限り、ノードをクリーンアップする必要があります。
Rust の型Rc<T>
を使用して明示的に複数の所有権を有効にする必要があります。これは、「参照カウント」の略です。Rc<T>
型は、値がまだ使用されているかどうかを判断するために、値への参照数を追跡します。値への参照がゼロの場合、値はクリーンアップされ、参照が無効になることはありません。
Rc<T>
を、リビングルームのテレビと想像してみてください。1 人がテレビを見るために部屋に入ると、テレビをオンにします。他の人が部屋に入ってテレビを見ることができます。最後の人が部屋を出ると、テレビをオフにします。なぜなら、もはや使用されていないからです。他の人がまだテレビを見ている間に誰かがテレビをオフにすると、残りのテレビ視聴者から大騒ぎが起こります!
プログラムの複数の部分でヒープ上にデータを割り当てて読み取りたい場合、コンパイル時にどの部分が最後にデータの使用を終えるかを判断できない場合、Rc<T>
型を使用します。最後に終了する部分がわかっていた場合、その部分をデータの所有者にすればよく、コンパイル時に強制される通常の所有権ルールが機能します。
ただし、Rc<T>
は単一スレッドのシナリオでのみ使用できます。16 章で並列性について説明する際に、マルチスレッドプログラムで参照カウントを行う方法について説明します。
<T>
{=html}を使ってデータを共有する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
の呼び出しは無視することができます。
<T>
{=html}をクローンすると参照カウントが増える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>
{=html}を使った参照サイクルの防止」で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