Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

ほとんどの場合、所有権は明確です。つまり、ある値をどの変数が所有しているのかが正確にわかります。しかし、1つの値に複数の所有者がいる場合もあります。たとえばグラフデータ構造では、複数のエッジが同じノードを指すことがあり、そのノードは概念的にはそれを指しているすべてのエッジに所有されています。ノードを指しているエッジがなくなり、所有者がいなくならないかぎり、そのノードは解放されるべきではありません。

複数の所有権を明示的に有効にするには、Rust の型 Rc<T> を使う必要があります。これは 参照カウント の略です。Rc<T> 型は、ある値への参照の数を追跡し、その値がまだ使用中かどうかを判断します。値への参照が 0 なら、その値はどの参照も無効にすることなく解放できます。

Rc<T> を、家族が使う居間のテレビだと考えてみてください。1人がテレビを見に部屋に入ったら、その人がテレビをつけます。ほかの人も部屋に入ってきて、そのテレビを見ることができます。最後の1人が部屋を出るとき、そのテレビはもう使われていないので消します。ほかの人がまだ見ているのに誰かがテレビを消したら、残っている視聴者は大騒ぎになるでしょう!

Rc<T> 型は、プログラムの複数の部分から読み取るためのデータをヒープに確保したいものの、そのデータの使用を最後に終えるのがどの部分なのかをコンパイル時に判断できない場合に使います。最後に終える部分がわかっているなら、その部分をデータの所有者にすればよく、コンパイル時に適用される通常の所有権規則がそのまま働きます。

Rc<T> はシングルスレッドのシナリオでのみ使用するものだという点に注意してください。第16章で並行性について説明するときに、マルチスレッドプログラムで参照カウントを行う方法を扱います。

データの共有

Listing 15-5 の cons リストの例に戻りましょう。これを Box<T> を使って定義したことを思い出してください。今回は、3つ目のリストの所有権を共有する2つのリストを作成します。概念的には、これは図15-3 に似ています。

ラベル 'a' の付いた連結リストが3つの要素を指しています。最初の要素には整数 5 が含まれ、2番目の要素を指しています。2
番目の要素には整数 10 が含まれ、3番目の要素を指しています。3番目の要素には、リストの終端を示す値 'Nil' が含まれて
います。この要素はどこも指していません。ラベル 'b' の付いた連結リストは、整数 3 を含む要素を指し、その要素はリスト 'a' の最
初の要素を指しています。ラベル 'c' の付いた連結リストは、整数 4 を含む要素を指し、その要素もリスト 'a' の最初の要素を指して
いるため、リスト 'b' と 'c' の末尾はどちらもリスト 'a' です。

図15-3: 2つのリスト bc が、 3つ目のリスト a の所有権を共有している

5、続いて 10 を含むリスト a を作成します。次に、さらに2つのリストを作ります。3 で始まる b と、4 で始まる c です。その後、bc の両方のリストは、510 を含む最初の a リストへと続きます。言い換えると、両方のリストが 510 を含む最初のリストを共有することになります。

Listing 15-17 に示すように、Box<T> を使った List の定義でこのシナリオを実装しようとしても、うまくいきません。

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));
    let c = Cons(4, Box::new(a));
}

このコードをコンパイルすると、次のエラーが出ます。

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
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
   |
note: if `List` implemented `Clone`, you could clone the value
  --> src/main.rs:1:1
   |
 1 | enum List {
   | ^^^^^^^^^ consider implementing `Clone` for this type
...
10 |     let b = Cons(3, Box::new(a));
   |                              - you could clone this value

For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` (bin "cons-list") due to 1 previous error

Cons バリアントは保持しているデータを所有しているため、b リストを作成するときに ab にムーブされ、ba を所有します。そのため、c を作成するときに再び a を使おうとしても、a はすでにムーブされているので許可されません。

代わりに参照を保持するように Cons の定義を変更することもできますが、そうするとライフタイム引数を指定しなければなりません。ライフタイム引数を指定するということは、リスト内のすべての要素が少なくともリスト全体と同じだけ生きることを指定することになります。これは Listing 15-17 の要素やリストについては成り立ちますが、あらゆるシナリオでそうであるとは限りません。

その代わりに、Listing 15-18 に示すように、List の定義を Box<T> の代わりに Rc<T> を使うよう変更します。これで各 Cons バリアントは、値と、List を指す Rc<T> を保持するようになります。b を作るときには、a の所有権を奪う代わりに、a が保持している Rc<List> をクローンします。これにより参照の数は 1 から 2 に増え、ab がその Rc<List> 内のデータの所有権を共有できるようになります。c を作るときにも a をクローンするので、参照の数は 2 から 3 に増えます。Rc::clone を呼び出すたびに、Rc<List> 内のデータへの参照カウントが増加し、参照が 0 にならないかぎりデータは解放されません。

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<T> は prelude に含まれていないため、スコープに導入する use 文を追加する必要があります。main では、510 を保持するリストを作成し、それを新しい Rc<List> として a に格納します。その後、bc を作成するときに、Rc::clone 関数を呼び出し、a に入っている Rc<List> への参照を引数として渡します。

Rc::clone(&a) ではなく a.clone() と呼ぶこともできましたが、この場合は Rc::clone を使うのが Rust の慣例です。Rc::clone の実装は、多くの型の clone 実装のようにすべてのデータをディープコピーしません。Rc::clone の呼び出しは参照カウントを増やすだけで、ほとんど時間がかかりません。データのディープコピーには多くの時間がかかることがあります。参照カウントのために Rc::clone を使うことで、ディープコピーを行う種類のクローンと、参照カウントを増やす種類のクローンを見た目で区別できます。コード中の性能上の問題を探すときは、ディープコピーを行うクローンだけを考慮すればよく、Rc::clone の呼び出しは無視できます。

クローンして参照カウントを増やす

Listing 15-18 の動作する例を変更して、a 内の Rc<List> への参照を作成したり破棄したりするにつれて、参照カウントがどのように変化するかを見てみましょう。

Listing 15-19 では、c リストのまわりに内側のスコープを持つように main を変更します。そうすると、c がスコープを抜けたときに参照カウントがどう変わるかを確認できます。

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

// --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));
}

プログラム内で参照カウントが変化する各時点で、Rc::strong_count 関数を 呼び出して取得した参照カウントを表示しています。この関数が count ではなく strong_count という名前になっているのは、Rc<T> 型には weak_count も あるからです。weak_count が何に使われるのかは、 Weak<T> を使って参照サイクルを防ぐ」 で見ていきます。

このコードは次のように出力します。

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.45s
     Running `target/debug/cons-list`
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

aRc<List> は初期参照カウントが 1 であり、その後 clone を呼び出す たびにカウントが 1 ずつ増えることがわかります。c がスコープを抜けると、 カウントは 1 減ります。参照カウントを増やすために Rc::clone を呼び出す 必要があるのに対して、参照カウントを減らすために関数を呼び出す必要はあり ません。Rc<T> の値がスコープを抜けると、Drop トレイトの実装が自動的に 参照カウントを減らしてくれるからです。

この例では見えませんが、main の終わりで b、続いて a がスコープを 抜けると、カウントは 0 になり、Rc<List> は完全にクリーンアップされます。 Rc<T> を使うと、単一の値が複数の所有者を持てるようになり、このカウントに よって、所有者のいずれかがまだ存在している限り、その値が有効なままである ことが保証されます。

不変参照を介して、Rc<T> はプログラムの複数の部分の間で、読み取り専用として データを共有できるようにします。もし Rc<T> が複数の可変参照も持てるように してしまうと、第 4 章で説明した借用ルールの 1 つに違反する可能性があります。 同じ場所に対する複数の可変借用は、データ競合や不整合を引き起こす可能性が あるからです。しかし、データを変更できることは非常に有用です! 次の節では、 内部可変性パターンと、この不変性の制約に対処するために Rc<T> と組み合わせて 使える RefCell<T> 型について説明します。