コンテンツにスキップ

Rustのスマートポインタ

所有権と借用のルールで、たいていの場面はうまくいきます。でも現実には、「所有者はちょうどひとり」という形にきれいに収まらないケースがいくつかあります。実行するまでサイズがわからない値、本当に複数の所有者が必要なデータ、共有された参照を通して書き換えたい値などです。スマートポインタ(smart pointer)は、そうしたケースそれぞれに対する Rust の答えです。

ふだん、値はスタックに置かれ、コンパイラはそのサイズを前もって知っている必要があります。Box<T> は値をヒープに移し、そこを指す固定サイズのポインタを渡してくれます。型のサイズがコンパイル時にわからないとき、特に再帰的な型で役に立ちます。

fn main() {
let boxed = Box::new(5);
println!("{}", *boxed); // * で中の値を取り出す
}

実行結果:

5
▶ 実際に試してみよう

Box がどうしても必要になる典型的な例は、連結リストのノードのように、自分自身を含む型です。

enum List {
Cons(i32, Box<List>),
Nil,
}
fn main() {
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
match list {
List::Cons(value, _) => println!("最初の値: {value}"),
List::Nil => println!("空です"),
}
}

実行結果:

最初の値: 1
▶ 実際に試してみよう

Box がないと、Cons(i32, List) を保存するには無限の領域が必要になってしまいます。List の中に List があり、その中にまた List があり……と永遠に続くからです。Box<List> なら固定サイズのポインタなので、コンパイラは必要な大きさを計算できます。

所有権のルールでは、値の所有者はちょうどひとりです。ほとんどの場合はそれで正解なのですが、プログラムの複数の部分で同じデータを本当に共有したいこともあります。Rc<T>(reference counted、参照カウント)を使うと、今の所有者が何人いるかを数えておくことで、それができるようになります。

use std::rc::Rc;
fn main() {
let a = Rc::new(String::from("共有データ"));
let b = Rc::clone(&a); // 中身を丸ごとコピーするのではなく、所有者が1人増えるだけ
let c = Rc::clone(&a);
println!("{a} {b} {c}");
println!("所有者の数: {}", Rc::strong_count(&a));
}

実行結果:

共有データ 共有データ 共有データ
所有者の数: 3
▶ 実際に試してみよう

Rc::clone は中身のデータをコピーしません。カウンターを1つ増やして、同じ値への参照をもう1つ返すだけです。データが片付けられるのは、すべての所有者がスコープの外に出たときです。

RefCell<T>:共有された参照を通して書き換える

Section titled “RefCell<T>:共有された参照を通して書き換える”

ふつう、借用のルールはコンパイル時にチェックされます。読み取り専用の参照をたくさん持つか、可変の参照を1つだけ持つか。両方を同時に持つことはできません。RefCell<T> は、この同じチェックを実行時に行います。そのおかげで、見た目は共有された mut なしの参照からでも、値を書き換えられるようになります。

use std::cell::RefCell;
fn main() {
let value = RefCell::new(5);
*value.borrow_mut() += 1;
println!("{}", value.borrow());
}

実行結果:

6
▶ 実際に試してみよう

RefCell は Rc と組み合わせて使うことがほとんどです。Rc<RefCell<T>> にすると、複数の所有者を持ち、しかも書き換えもできる値になります。これは Rc<T> だけではできないことです。

実際にこれらを使うのはいつ?

Section titled “実際にこれらを使うのはいつ?”

最初からスマートポインタを使おうとしないでください。まずはふつうの所有権と借用で書きましょう。スマートポインタを使うのは、コンパイラが「それではうまくいかない」とはっきり言ってきたときだけです。再帰的な型なら Box、本当に複数の所有者が必要な値なら Rc、借用のルールでは許されない「共有された参照を通しての書き換え」が必要なら RefCell です。

Box の節で使った List 列挙型を使います。1、2、3 のリストを作り、すべての値を足し合わせる関数 fn sum(list: &List) -> i32 を書いてください。ヒント:リストを match で場合分けします。Cons(value, rest) なら答えは value に rest の合計を足したもの、Nil なら 0 です。

解答を見る
enum List {
Cons(i32, Box<List>),
Nil,
}
fn sum(list: &List) -> i32 {
match list {
List::Cons(value, rest) => value + sum(rest),
List::Nil => 0,
}
}
fn main() {
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Cons(3, Box::new(List::Nil))))));
println!("合計: {}", sum(&list));
}

実行結果:

合計: 6
▶ 実際に試してみよう

この関数は、Nil にたどり着くまで、リストの残りの部分に対して自分自身を呼び出します。&List を受け取る場所に rest(型は &Box<List>)をそのまま渡せていることにも注目してください。Rust が Box の中身を自動的に見てくれるのです。

alice と bob の2人が両方とも所有し、どちらからも追加できる買い物リスト(Vec<String>)を1つ作ってください。それぞれが品物を1つずつ追加したら、リストと所有者の数を表示します。ヒント:Vec を Rc<RefCell<...>> で包み、それぞれに Rc::clone を渡して、.borrow_mut().push(...) で追加します。

解答を見る
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let list = Rc::new(RefCell::new(Vec::new()));
let alice = Rc::clone(&list);
let bob = Rc::clone(&list);
alice.borrow_mut().push(String::from("牛乳"));
bob.borrow_mut().push(String::from("パン"));
println!("{:?}", list.borrow());
println!("所有者の数: {}", Rc::strong_count(&list));
}

実行結果:

["牛乳", "パン"]
所有者の数: 3
▶ 実際に試してみよう

所有者は list、alice、bob の3人です。3つとも同じ Vec を指しているので、alice から追加した品物は bob からも list からも見えます。書き換えは RefCell が引き受けてくれるので、どの変数にも mut は必要ありません。

  • Box<T> は値をヒープに置き、固定サイズのポインタで指します。再帰的な型には欠かせません。
  • Rc<T> は参照カウントによって、シングルスレッドのコードで同じ値に複数の所有者を持たせます。スレッドをまたぐなら Arc<T> を使います。
  • RefCell<T> は借用のルールをコンパイル時ではなく実行時にチェックし、共有された参照を通した書き換えを可能にします。ただし、ルールを破るとパニックします。
  • これらを使うのは、ふつうの所有権と借用ではうまくいかないときだけにしましょう。

理解度チェック

1. 連結リストのノードのような再帰的な型に Box<T> が必要なのはなぜですか?

2. Rc::clone(&a) は実際に何をしますか?

3. RefCell<T> が借用のルールをチェックするのはいつですか?

4. Rc<RefCell<T>> にすると、Rc<T> だけではできない何ができるようになりますか?

スコア: 0 / 4