スマートポインタ Rc<T> - 参照カウンタ

Rc<T>, the Reference Counted Smart Pointer

所有権は多くの場合において明白である。つまり「1変数は、1つの値の所有する 」。

しかしながら 一つの値が複数の所有者を持つ ケースが存在する。 例として、グラフ構造がある。 グラフは、複数の辺が同一のノードを指し示しす。つまりノードは概念的にそれを指し示す全ての辺によって所有される。 またノードは、それを指し示す辺がある限り、削除されるべきではない。

Rustは、複数の所有権を可能にするために、Rc<T>型(参照カウンタ(reference counting)と称される)を持つ。

Rc<T>型は、値が使われているかどうかを決定するための値への参照の数 をトラックし続ける。 もし値への参照数が0になれば、その時、値は消去される。

Rc<T>を部屋に置かれたテレビとすれば、一人の人間がそのテレビをつけると、その他の人も部屋に入ってテレビを見ることができ、 最後の人間が離れると、もはや使用されていないので、テレビを消すことができる。 もし誰かが他の人が見ているのに、テレビを消した場合、大論争になるかもしれません。

とあるデータをプログラムのあちこちで読むためにヒープ上に配置したいとき、そしてどの箇所が最後に使用されるのか決定できないときに、Rc<T>型を使用します。

万が一、もし最後に使用する場所が分かっているならば、その場所をまさに所有者と決定することができるので、コンパイル時間に強制される通常の所有権ルールが効果を発揮するかもしれません。

Note:

Rc<T>型は1つのスレッド上のみで動作させる。マルチスレッドにおける参照カウントは、こちら

Rc<T> - データの共有

以下のcons listを考える。b,cからのノードは直接aを指し示すノードを参照している。

f:id:yossan2:20210221140524p:plain
Cons List

これをBox<T>型で書くと、所有権の問題が発生する。

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

fn main() {
    let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
    let b = Cons(3, Box::new(a)); // aの所有権が移動する
    let c = Cons(4, Box::new(a)); // エラーが発生する

もしくはこちらのようにaへの参照を直接持つようにすると、ライフタイムを明示する必要が発生する。

enum List<'a> {
    Cons(i32, &'a List<'a>),
    Nil,
}


use crate::List::{Cons, Nil};
// リファレンスでは動かないとあるが、v1.50では動く
let a = Cons(5, &Cons(10, &Nil));
let b = Cons(3, &a);
let c = Cons(4, &a);

(v1.50以前では、借用チェッカーが、&Cons(10, &Nil)コンパイルさせることを許可しない。なぜならば変数aが一時的なNil値の参照を取る前にドロップさせてしまうため。)

その代わりに、Box<T>からRc<T>型に変更する。

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

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

let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); // a: Rc<List>
let b = Cons(3, Rc::clone(&a)); // Rc::cloneによって、aへの参照カントを増やす
let c = Cons(4, Rc::clone(&a)); // Rc::cloneによって、aへの参照カウントを増やす

Consは、値(i32)とListを指し示すRc<T>を持つ。

変数bを作るとき、aの所有権を取る代わりに、aをホールドするRc<List>をクローンし、aへの参照カウントを増やす。 毎回Rc::cloneを呼ぶごとに、Rc<List>を内部に含んでいるデータへの参照カウントは増加すし、その値が0になるまでは、データは消去されない。 Rc<T>を使うには、useステートメントを使ってスコープに追加する必要がある。プレリュード(前奏曲)にないため。

このケースに置いては、Rc::clone(&a)の代わりに、a.clone()を呼ぶことができるが、Rustの慣習では、Rc::cloneを使用する。

Rc::cloneは、ディープコピーを作るのではなく、参照カウントをインクリメントするのみである。

Rc::cloneによって、ディープコピーをするクローンと参照カウントを増加するクローンの違いを視覚的にすることができる。 これはコードのパフォーマンスの問題を探す際に、Rc::cloneを呼び出しを無視することができ、ディープコピークローンを考えるだけですむようになる。

Cloning an Rc<T> - 参照カウントのインクリメント

変数aのRc<List>への参照を作成・ドロップさせることで、参照カウントの変更を見ていく。

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

use crate::List::{Cons, Nil};
// a: Rc<List>
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));

/*
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2
*/

Rc::strong_count関数を呼ぶことで、参照カウントを取得することができる。 この関数は、countではなく、strong_count命名されているのは、Rc<T>型は、weak_countという関数をも持つためである。 weak_countについては、こちらを参照。

イミュータブル参照を通して、Rc<T>は、複数のプログラムのあちこちの間で、データへの参照を許可する。 もしRc<T>が、ミュータブルな参照を許可することがあれば、借用ルールを破壊することになる。

同じデータへの複数のミュータブルな参照は、データ競合と矛盾を引き起こす。

しかし、可変データは非常に有用である。 次のセクションでは、内部の可変パターンとRefCell<T>について議論する。

RefCell<T>型は、この不可変な制限をもつRc<T>とともに使用することができる。

まとめ

Rc<T>によって

  • スマートポインタの一種
  • ヒープ上の値への参照を、参照ではなく値(Rc<T>型)として保持することができる
  • イミュータブルな参照を実現
  • 参照では必要となるライフタイムの明示を除去

関連と参照

doc.rust-lang.org