Rustのスマートポインタ
所有権と借用のルールで、たいていの場面はうまくいきます。でも現実には、「所有者はちょうどひとり」という形にきれいに収まらないケースがいくつかあります。実行するまでサイズがわからない値、本当に複数の所有者が必要なデータ、共有された参照を通して書き換えたい値などです。スマートポインタ(smart pointer)は、そうしたケースそれぞれに対する Rust の答えです。
Box<T>:値をヒープに置く
Section titled “Box<T>:値をヒープに置く”ふだん、値はスタックに置かれ、コンパイラはそのサイズを前もって知っている必要があります。Box<T> は値をヒープに移し、そこを指す固定サイズのポインタを渡してくれます。型のサイズがコンパイル時にわからないとき、特に再帰的な型で役に立ちます。
fn main() { let boxed = Box::new(5); println!("{}", *boxed); // * で中の値を取り出す}実行結果:
5Box がどうしても必要になる典型的な例は、連結リストのノードのように、自分自身を含む型です。
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!("空です"), }}実行結果:
最初の値: 1Box がないと、Cons(i32, List) を保存するには無限の領域が必要になってしまいます。List の中に List があり、その中にまた List があり……と永遠に続くからです。Box<List> なら固定サイズのポインタなので、コンパイラは必要な大きさを計算できます。
Rc<T>:複数の所有者
Section titled “Rc<T>:複数の所有者”所有権のルールでは、値の所有者はちょうどひとりです。ほとんどの場合はそれで正解なのですが、プログラムの複数の部分で同じデータを本当に共有したいこともあります。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));}実行結果:
共有データ 共有データ 共有データ所有者の数: 3Rc::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());}実行結果:
6RefCell は Rc と組み合わせて使うことがほとんどです。Rc<RefCell<T>> にすると、複数の所有者を持ち、しかも書き換えもできる値になります。これは Rc<T> だけではできないことです。
実際にこれらを使うのはいつ?
Section titled “実際にこれらを使うのはいつ?”最初からスマートポインタを使おうとしないでください。まずはふつうの所有権と借用で書きましょう。スマートポインタを使うのは、コンパイラが「それではうまくいかない」とはっきり言ってきたときだけです。再帰的な型なら Box、本当に複数の所有者が必要な値なら Rc、借用のルールでは許されない「共有された参照を通しての書き換え」が必要なら RefCell です。
1. 連結リストの合計を求める
Section titled “1. 連結リストの合計を求める”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 の中身を自動的に見てくれるのです。
2. みんなで使う買い物リスト
Section titled “2. みんなで使う買い物リスト”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>は借用のルールをコンパイル時ではなく実行時にチェックし、共有された参照を通した書き換えを可能にします。ただし、ルールを破るとパニックします。- これらを使うのは、ふつうの所有権と借用ではうまくいかないときだけにしましょう。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. 連結リストのノードのような再帰的な型に Box<T> が必要なのはなぜですか?
2. Rc::clone(&a) は実際に何をしますか?
3. RefCell<T> が借用のルールをチェックするのはいつですか?
4. Rc<RefCell<T>> にすると、Rc<T> だけではできない何ができるようになりますか?
スコア: 0 / 4
さらに詳しく
Section titled “さらに詳しく”- ヒープのデータを指す
Box<T>を使用する — The Rust Programming Language 日本語版:このページでも使ったコンスリストの例を含め、Boxについて一通り説明しています。 Rc<T>は、参照カウント方式のスマートポインタ — The Rust Programming Language 日本語版:カウントが増えたり減ったりする様子を見ながら、共有された所有権を一歩ずつ学べます。RefCell<T>と内部可変性パターン — The Rust Programming Language 日本語版:借用チェックを実行時に回すのはいつ、なぜなのかを、現実的な例で解説しています。std::cell— 標準ライブラリのドキュメント(英語):CellとRefCellの説明と、どちらを選ぶべきかがわかります。- Rc — Rust By Example 日本語版:所有者が増えたり減ったりするたびに参照カウントを表示する、短くて実行できる例です。