Rustのスマートポインタ
Box、Rc、RefCell、Arcを使って基本的な参照を超えましょう。スマートポインタのヒープ割り当てと共有所有権について学びます。
中級Rustシリーズの13回目です。通常の参照(&T)はメモリへの借用ビューにすぎませんが、スマートポインタ(Smart Pointers)は、指し示すデータを所有するデータ構造です。参照のように動作しますが、追加のメタデータと機能を備えています — ヒープ割り当て、共有所有権、内部可変性。
graph TD
A["スマートポインタ"] --> B["Box<T><br/>ヒープ割り当て<br/>単一の所有者"]
A --> C["Rc<T><br/>参照カウント<br/>シングルスレッド共有"]
A --> D["RefCell<T><br/>内部可変性<br/>実行時借用チェック"]
A --> E["Arc<T><br/>アトミック参照カウント<br/>スレッドセーフ共有"]
A --> F["Derefトレイト<br/>*演算子のカスタマイズ"]
何がポインタを「スマート」にするのか?#
通常の参照(&T)は単なるアドレス — 他の誰かが所有するデータを借用するものです。スマートポインタは、逆に、指し示すデータを所有します。スマートポインタがスコープから外れると、データは自動的にドロップされ、メモリが解放されます。
その魔法の鍵は Deref トレイトです。これにより、スマートポインタは * 演算子を使用するときの動作をカスタマイズでき、通常の参照のように振る舞うことができます。
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}rustDeref を実装することで、*my_box を *reference のように使用でき、Rust は背後で自動的に deref() を呼び出します。
Box:データをヒープに配置する#
Box<T> は最もシンプルなスマートポインタです。データをヒープ上に割り当て、単一の所有者を提供します。Box がドロップされると、ヒープメモリが解放されます。
Boxを使用する理由#
- 再帰的型: 自分自身を含む型(リンクリストやツリーノードなど)は、既知のサイズを持つために間接参照が必要です。
- 大きなデータの転送: スタック上のコピーを避けるために、大きなデータをヒープに移動します。
- トレイトオブジェクト: 共通のインターフェースの背後に異なる型の値を格納します。
Boxを使った再帰的型#
Box がなければ、リンクリストのような再帰的な構造体は無限のサイズになってしまいます:
// ❌ コンパイルできません — Listは無限のサイズ
enum List {
Cons(i32, List),
Nil,
}
// ✅ Boxは間接参照を追加して循環を断ち切ります
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
// 1 -> 2 -> 3 -> Nil
}rustgraph LR
A["Cons(1, Box)"] --> B["Cons(2, Box)"]
B --> C["Cons(3, Box)"]
C --> D["Nil"]
トレイトオブジェクトのためのBox#
Box<dyn Trait> は、異なる型を一つのインターフェースの背後に格納できます:
trait Draw {
fn draw(&self);
}
struct Button;
struct Slider;
impl Draw for Button {
fn draw(&self) { println!("ボタンを描画"); }
}
impl Draw for Slider {
fn draw(&self) { println!("スライダーを描画"); }
}
fn main() {
let components: Vec<Box<dyn Draw>> = vec![
Box::new(Button),
Box::new(Slider),
];
for component in &components {
component.draw();
}
}rustRc:共有所有権(シングルスレッド)#
時には、同じデータに複数の所有者が必要な場合があります。Rc<T>(Reference Counted - 参照カウント)は、シングルスレッド環境でこれを可能にします。
仕組み#
Rc::clone() を呼び出すたびに、参照カウントが増えます。クローンがドロップされると、カウントが減ります。カウントが0になったときに初めてデータが解放されます。
use std::rc::Rc;
fn main() {
let data = Rc::new(String::from("こんにちは、共有の世界!"));
println!("参照カウント: {}", Rc::strong_count(&data)); // 1
{
let data2 = Rc::clone(&data); // カウントが増える
println!("参照カウント: {}", Rc::strong_count(&data)); // 2
println!("data2: {data2}");
} // data2がドロップされ、カウントが減る
println!("参照カウント: {}", Rc::strong_count(&data)); // 1
println!("data: {data}");
}rustRcを使ったツリー構造#
Rc<T> は、子ノードが親ノードを参照する必要があるツリー構造に最適です:
use std::cell::RefCell;
use std::rc::Rc;
struct Node {
value: i32,
children: Vec<Rc<Node>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
children: vec![],
});
let branch = Rc::new(Node {
value: 5,
children: vec![Rc::clone(&leaf)],
});
println!("leafの参照カウント: {}", Rc::strong_count(&leaf)); // 2
}rust[!NOTE]
Rc<T>は非アトミック参照カウントを使用するため、高速ですがスレッドセーフではありません。マルチスレッドコードでは、代わりにArc<T>を使用してください。
RefCell:内部可変性#
RefCell<T> は内部可変性を可能にします — RefCell 自体への不変参照しか持たない場合でも、データを変更できる機能です。
実行時の借用チェッカー#
Rc<T> が所有者の数を追跡するのに対し、RefCell<T> は実行時に参照の数を追跡します。ルールはコンパイル時の借用と同じですが、違反は実行時パニックを引き起こします。
use std::cell::RefCell;
fn main() {
let cell = RefCell::new(5);
{
let mut borrow = cell.borrow_mut(); // 可変借用
*borrow += 1;
println!("ブロック内: {borrow}");
} // borrowはここでドロップされる
println!("ブロック後: {}", cell.borrow()); // 不変借用
}rustRc + RefCell の組み合わせ#
Rc<T> と RefCell<T> を組み合わせると、共有所有権と変更可能性の両方が得られます:
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let data = Rc::new(RefCell::new(vec![1, 2, 3]));
let data2 = Rc::clone(&data);
data2.borrow_mut().push(4);
println!("data: {:?}", data.borrow()); // [1, 2, 3, 4]
}rust[!WARNING]
RefCell<T>は借用ルールに違反した場合(例:同時に2つの可変借用)に実行時にパニックします。これはコンパイル時の安全性を柔軟性と交換するものです — 適切に使用してください。
Arc:スレッドセーフな共有所有権#
Arc<T>(Atomically Reference Counted - アトミック参照カウント)は、Rc<T> のマルチスレッド版です。アトミック操作を使用して、スレッド間でデータを安全に共有します。
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3, 4, 5]);
let mut handles = vec![];
for _ in 0..4 {
let data = Arc::clone(&data);
let handle = thread::spawn(move || {
let sum: i32 = data.iter().sum();
println!("スレッド合計: {sum}");
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
}rustgraph TD
A["Arc<Vec>"] --> B["スレッド 1<br/>Arc::clone"]
A --> C["スレッド 2<br/>Arc::clone"]
A --> D["スレッド 3<br/>Arc::clone"]
A --> E["スレッド 4<br/>Arc::clone"]
[!TIP]
Arc<T>はアトミック操作により若干のパフォーマンスコストがかかります。スレッドセーフティが必要ない場合は、Rc<T>の方が高速です。
どのスマートポインタを使うべきか?#
| スマートポインタ | 使用ケース | スレッドセーフ? |
|---|---|---|
Box<T> | ヒープ割り当て、再帰的型、トレイトオブジェクト | ✅ はい(単一の所有者) |
Rc<T> | 複数の所有者、シングルスレッド | ❌ いいえ |
RefCell<T> | 内部可変性、実行時借用チェック | ❌ いいえ |
Arc<T> | 複数の所有者、マルチスレッド | ✅ はい |
Rc<RefCell<T>> | 共有 + 可変、シングルスレッド | ❌ いいえ |
Arc<Mutex<T>> | 共有 + 可変、マルチスレッド | ✅ はい |
分かりやすい例え(ELI5):図書館の本管理システム#
スマートポインタを図書館の本の管理方法に例えて考えてみましょう:
Box<T>は本を借りることに似ています — あなたは唯一の所有者です。返却(drop)すると、図書館は本を回収します。Rc<T>は、複数の研究グループが一緒に借りることができる本に似ています。図書館は借りているグループの数を追跡します。最後のグループが返却したときに初めて図書館が回収します。RefCell<T>は、「読み取り専用」のカバーが付いた本に似ています。あなたは見ることができますが、メモを書くには、図書館員(実行時チェッカー)から特別な許可を得る必要があります — そして一度に書けるのは一人だけです。Arc<T>は、複数のキャンパスが同じ本を共有する大学規模の図書館システムのRc<T>です。アトミックカウンターにより、どのキャンパスも本を見失うことはありません。
まとめ#
- スマートポインタはデータを所有し、
Derefを実装して参照のように振る舞います。 Box<T>はヒープ割り当てを提供し、再帰的型やトレイトオブジェクトに不可欠です。Rc<T>は参照カウントにより、シングルスレッドコードで複数の所有者を可能にします。RefCell<T>は実行時借用チェックにより、内部可変性を提供します。Arc<T>はマルチスレッドコード向けのRc<T>のスレッドセーフ版です。- これらを組み合わせる(例:
Rc<RefCell<T>>やArc<Mutex<T>>)により、共有可能で変更可能な状態を管理する強力なパターンが得られます。
次回の記事では、Functional Rust — クロージャとイテレータ — について探求し、Rustのゼロコスト抽象化がどのように表現力の高い高レベルコードを書くことができるかを学びます。
参照#
- The Rust Programming Language - Chapter 15: Smart Pointers ↗
- The Rust Programming Language - Chapter 15.1: Box ↗
- The Rust Programming Language - Chapter 15.4: Rc ↗
- The Rust Programming Language - Chapter 15.5: RefCell ↗
- The Rust Programming Language - Chapter 16.3: Arc ↗
- Rust by Example - Box, Stack and Heap ↗