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のような見た目になります:

3番目のリストの所有権を共有する2つのリスト

図15-3: 3番目のリスト、aの所有権を共有する2つのリスト、bc

5と10を含むリストaを作ります。さらにもう2つリストを作ります: 3で始まるbと4で始まるcです。 bcのどちらもそれから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));
}

リスト15-17: 3番目のリストの所有権を共有しようとするBox<T>を使った2つのリストを存在させることはできないとデモする

このコードをコンパイルすると、こんなエラーが出ます:

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リストを作成する時に、 abにムーブされ、baを所有します。それから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に増え、abにその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));
}

リスト15-18: Rc<T>を使用するListの定義

初期化処理に含まれていないので、use文を追加してRc<T>をスコープに導入する必要があります。 mainで5と10を保持するリストを作成し、aの新しいRc<List>に格納しています。それから、 bcを作成する際に、Rc::clone関数を呼び出し、引数としてaRc<List>への参照を渡しています。

Rc::clone(&a)ではなく、a.clone()を呼ぶこともできますが、Rustのしきたりは、この場合Rc::cloneを使うことです。 Rc::cloneの実装は、多くの型のclone実装のように、全てのデータのディープコピーをすることではありません。 Rc::cloneの呼び出しは、参照カウントをインクリメントするだけであり、時間はかかりません。 データのディープコピーは時間がかかることもあります。参照カウントにRc::cloneを使うことで、 視覚的にディープコピーをする類のクローンと参照カウントを増やす種類のクローンを区別することができます。 コード内でパフォーマンスの問題を探す際、ディープコピーのクローンだけを考慮し、Rc::cloneの呼び出しを無視できるのです。

Rc<T>をクローンすると、参照カウントが増える

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

リスト15-19: 参照カウントを出力する

プログラム内で参照カウントが変更される度に、参照カウントを出力します。参照カウントは、 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

aRc<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>型について議論します。

関連キーワード:  Rc, リスト, Cons, List, count, データ, 参照, let, new, Nil