スマートポインタとは?
Rustには「スマートポインタ」と呼ばれる、通常の参照よりも多くの機能を持つ型が標準ライブラリに用意されています。スマートポインタはデータへのポインタとして機能しつつ、メタデータや追加の能力を持ちます。
代表的なものとして次の3つがあります。
Box<T>:ヒープ上にデータを確保するRc<T>:参照カウント方式で複数の所有者を持てるRefCell<T>:コンパイル時ではなく実行時に借用ルールをチェックする
それぞれの役割と使い方を順番に見ていきましょう。
Box<T>:ヒープへのシンプルな割り当て
基本的な使い方
Box<T> は最もシンプルなスマートポインタです。値をスタックではなくヒープに置きたいときに使います。
fn main() {
let x = Box::new(5);
println!("x = {}", x); // x = 5
}
*x で中身を取り出す(デリファレンス)こともできます。
fn main() {
let x = Box::new(10);
let y = *x + 5;
println!("y = {}", y); // y = 15
}
再帰的なデータ構造への応用
Box<T> が特に役立つのが、再帰的なデータ構造の定義です。たとえば連結リストを素直に定義しようとすると、コンパイラはそのサイズを計算できずにエラーになります。
// これはコンパイルエラーになる
// enum List {
// Cons(i32, List),
// Nil,
// }
// Boxを使えばOK
enum List {
Cons(i32, Box<List>),
Nil,
}
fn main() {
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
}
Box<List> はポインタなのでサイズが固定され、コンパイルが通ります。
Rc<T>:複数の所有者を持つ参照カウント
なぜ複数の所有者が必要か
Rustの所有権システムでは、通常1つの値に所有者は1人だけです。しかし、グラフ構造やGUIのイベント処理など、複数の箇所から同じデータを参照したいケースがあります。そのために Rc<T>(Reference Counted)が用意されています。
Rc<T>の基本
use std::rc::Rc;
fn main() {
let a = Rc::new(String::from("hello"));
println!("参照カウント: {}", Rc::strong_count(&a)); // 1
let b = Rc::clone(&a);
println!("参照カウント: {}", Rc::strong_count(&a)); // 2
{
let c = Rc::clone(&a);
println!("参照カウント: {}", Rc::strong_count(&a)); // 3
}
// cがスコープを抜けるとカウントが減る
println!("参照カウント: {}", Rc::strong_count(&a)); // 2
}
Rc::clone はデータをコピーするのではなく、参照カウントをインクリメントするだけなので低コストです。カウントが0になった時点でメモリが解放されます。
⚠️
Rc<T>はシングルスレッド専用です。マルチスレッド環境ではArc<T>(Atomically Reference Counted)を使ってください。
RefCell<T>:実行時の借用チェックと内部可変性
内部可変性とは
Rustでは通常、不変参照(&T)を通じてデータを変更することはできません。しかし RefCell<T> を使うと、不変参照を持っていても中身を変更できる「内部可変性」が得られます。借用ルールはコンパイル時ではなく実行時にチェックされます。
RefCell<T>の基本
use std::cell::RefCell;
fn main() {
let data = RefCell::new(vec![1, 2, 3]);
// 不変スコープの中でも変更できる
data.borrow_mut().push(4);
println!("{:?}", data.borrow()); // [1, 2, 3, 4]
}
borrow() で不変参照、borrow_mut() で可変参照を取得します。実行時に借用ルール違反があると panic! が発生します。
Rc<T>とRefCell<T>の組み合わせ
Rc<T> と RefCell<T> を組み合わせると、複数の所有者から変更できるデータが実現できます。
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
let shared = Rc::new(RefCell::new(0));
let a = Rc::clone(&shared);
let b = Rc::clone(&shared);
*a.borrow_mut() += 10;
*b.borrow_mut() += 20;
println!("値: {}", shared.borrow()); // 値: 30
}
この組み合わせはRustで非常によく使われるパターンです。
3つのスマートポインタの使い分け
| 型 | 所有者 | 変更 | スレッド |
|---|---|---|---|
Box<T> | 1人 | 通常の借用ルール | ✅ |
Rc<T> | 複数 | 不変のみ | ❌ (シングルのみ) |
RefCell<T> | 1人 | 内部可変性あり | ❌ (シングルのみ) |
Rc<RefCell<T>> | 複数 | 内部可変性あり | ❌ (シングルのみ) |
まとめ
Box<T>:サイズが不定な型をヒープに置きたいとき、再帰データ構造を作るときに使うRc<T>:同じデータを複数箇所で所有したいときに使う(シングルスレッド限定)RefCell<T>:不変参照からでも変更したいとき(内部可変性)に使うRc<RefCell<T>>:複数の所有者から変更したいときの定番パターン
スマートポインタを使いこなすことで、Rustの所有権システムの制約の中でも柔軟なデータ設計が可能になります。最初は難しく感じるかもしれませんが、どのシーンで何を使うかを意識しながらコードを書くことで自然と身についていきます。