Rustのスマートポインタ: Box、Rc、Arc
1. 概要
デフォルトでは、Rust の各値はオーナーが 1 つだけで、Rust はコンパイル時に値のサイズを知る必要があります。多くの現実的な状況ではこの 2 つの条件を満たしていません: 再帰的なデータ型、複数の異なる型を含むリスト、あるいは複数のオーナーが必要なデータです。この記事では、最も一般的な 3 つのスマートポインタ Box<T>、Rc<T>、Arc<T> を紹介します。読み終わると、各状況に応じてどの型を選ぶべきかが分かり、Weak、RefCell、Mutex との組み合わせ方、そしてよくあるエラーについて理解できます。
2. スマートポインタとは?
スマートポインタはポインタをラップした struct で、2 つの重要な trait を実装しています:
Deref: スマートポインタを通常の参照のように使用できるようにします (*ptr、メソッドの直接呼び出し)。Drop: スマートポインタがスコープを抜けるときにリソースを自動的に解放します。
String と Vec<T> も実はスマートポインタです。Box、Rc、Arc の違いは、ヒープ上のデータの 所有権 の管理方法に重点を置いていることです。3 つとも標準ライブラリに含まれています。Cargo.toml に依存を追加する必要はありません。
3. Box<T>: 1 つのオーナー、ヒープ上のデータ
3.1. 基本的な割り当て
fn main() {
let order_total = Box::new(250_000u64);
println!("合計: {}", order_total);
let doubled = *order_total * 2;
println!("2 倍: {}", doubled);
}
Box::new は値 250_000 をヒープに割り当てます。スタック上には固定サイズのポインタだけが残ります。println! が機能するのは、T が Display を実装している場合、Box も Display を実装しているためです。演算子 * は Deref のおかげで内部の値を取得します。order_total が main を抜けるとき、ヒープメモリは自動的に解放されます。
合計: 250000
2 倍: 500000
上の例のように u64 を Box でラップしても利点はありません。このコードは単に構文を示しています。Box は次の 2 つのケースで本当に有用です。
3.2. 再帰的なデータ型
注文処理プロセスをステップチェーンとして説明するとします:
#[derive(Debug)]
enum OrderStep {
Done,
Next(String, Box<OrderStep>),
}
use OrderStep::{Done, Next};
fn count(step: &OrderStep) -> usize {
match step {
Done => 0,
Next(_, rest) => 1 + count(rest),
}
}
fn main() {
let flow = Next(
"注文".into(),
Box::new(Next(
"支払い".into(),
Box::new(Next("配送".into(), Box::new(Done))),
)),
);
println!("ステップ数: {}", count(&flow));
}
Box なしで Next(String, OrderStep) と書くと、コンパイラはエラーを出します:
error[E0072]: recursive type `OrderStep` has infinite size
理由は、OrderStep が別の OrderStep を含み、それがさらに別のものを含む…という具合に、計算上のサイズが無限になるからです。Box<OrderStep> は常にポインタと同じサイズなので、enum のサイズは有限になります。関数 count では、rest の型は &Box<OrderStep> です。Rust は deref coercion のおかげでこれを自動的に &OrderStep に変換します。
ステップ数: 3
3.3. トレイトオブジェクト
Box<dyn Trait> を使うと、異なる複数の型を同じコレクションに保存できます:
trait PaymentMethod {
fn pay(&self, amount: u64) -> String;
}
struct Cash;
struct Card {
last4: String,
}
impl PaymentMethod for Cash {
fn pay(&self, amount: u64) -> String {
format!("現金: {amount}")
}
}
impl PaymentMethod for Card {
fn pay(&self, amount: u64) -> String {
format!("カード ****{}: {amount}", self.last4)
}
}
fn main() {
let methods: Vec<Box<dyn PaymentMethod>> = vec![
Box::new(Cash),
Box::new(Card { last4: "4242".into() }),
];
for m in &methods {
println!("{}", m.pay(100_000));
}
}
Cash と Card は異なるサイズなので、直接同じ Vec に入れることはできません。Box<dyn PaymentMethod> でラップすると、すべての要素が同じサイズになります (fat pointer: データポインタと vtable ポインタの組み合わせ)。pay の呼び出しは実行時に解決されます。
現金: 100000
カード ****4242: 100000
注意: すべての要素が同じ型の場合、
Box<dyn …>の代わりに generic (impl PaymentMethodまたは<T: PaymentMethod>) を使用します。Generic は static dispatch を使用するため、ヒープ割り当ての費用がなく、vtable 経由の呼び出しも不要です。
4. Rc<T>: 単一スレッド内での複数のオーナー
4.1. 参照カウント
複数の Order が 1 つの Customer に属していると仮定します。各注文に顧客情報をコピーしたくなく、どの注文が最も長く “生存” してオーナーになるかも不明な場合があります。これが Rc (Reference Counted) を使う場面です:
use std::rc::Rc;
struct Customer {
name: String,
}
struct Order {
id: u32,
customer: Rc<Customer>,
}
fn main() {
let alice = Rc::new(Customer { name: "Alice".into() });
println!("作成後: {}", Rc::strong_count(&alice));
let o1 = Order { id: 1, customer: Rc::clone(&alice) };
{
let o2 = Order { id: 2, customer: Rc::clone(&alice) };
println!("ブロック内: {}", Rc::strong_count(&alice));
println!("注文 {} は {}", o2.id, o2.customer.name);
}
println!("ブロック後: {}", Rc::strong_count(&alice));
println!("注文 {} は {}", o1.id, o1.customer.name);
}
Rc::clone は Customer をコピーしません。strong count カウンターを 1 だけ増やすだけです。ブロックの終わりで o2 が drop されるとき、カウンターは 1 減ります。Customer はカウンターが 0 になったときだけ解放されます。
作成後: 1
ブロック内: 3
注文 2 は Alice
ブロック後: 2
注文 1 は Alice
慣例として、alice.clone() ではなく Rc::clone(&alice) と書くべきです。この書き方によって、これは安い参照カウント操作であり、deep copy ではないことが読み手に伝わります。
4.2. Rc<RefCell<T>> でデータを変更する
Rc<T> はイミュータブル参照 (&T) だけを提供します。複数のオーナーがデータを変更する必要がある場合は、RefCell と組み合わせます:
use std::cell::RefCell;
use std::rc::Rc;
struct Cart {
items: Vec<String>,
}
fn main() {
let cart = Rc::new(RefCell::new(Cart { items: vec![] }));
let web = Rc::clone(&cart);
let mobile = Rc::clone(&cart);
web.borrow_mut().items.push("シャツ".into());
mobile.borrow_mut().items.push("笠".into());
println!("{:?}", cart.borrow().items);
}
RefCell は borrow チェックをコンパイル時から実行時に移行します。borrow_mut() はガードを返します。このガードはステートメント終了時に有効期限が切れるため、次の呼び出しは有効なままです。
["シャツ", "笠"]
注意: 別の
borrow()またはborrow_mut()がまだ生存中にborrow_mut()を呼び出すと、プログラムはalready borrowed: BorrowMutErrorでパニックします。ガードは可能な限り短い時間だけ保持してください。panic の代わりにエラーを処理したい場合はtry_borrow_mut()を使用してください。
4.3. 循環参照と Weak<T>
Customer が Rc<Order> のリストを持ち、各 Order が Rc<Customer> を持つと、2 つのカウンターは決して 0 にはなりません。その結果、メモリリークが発生します。Rust はこのエラーをコンパイル時に防ぐことはできません。解決策は、参照の一方向に Weak を使用することです:
use std::cell::RefCell;
use std::rc::{Rc, Weak};
struct Customer {
name: String,
orders: RefCell<Vec<Rc<Order>>>,
}
struct Order {
id: u32,
customer: Weak<Customer>,
}
fn main() {
let bob = Rc::new(Customer {
name: "Bob".into(),
orders: RefCell::new(vec![]),
});
let order = Rc::new(Order { id: 7, customer: Rc::downgrade(&bob) });
bob.orders.borrow_mut().push(Rc::clone(&order));
if let Some(c) = order.customer.upgrade() {
println!("注文 {} は {}", order.id, c.name);
}
println!("strong={}, weak={}", Rc::strong_count(&bob), Rc::weak_count(&bob));
drop(bob);
println!("顧客はまだいますか? {}", order.customer.upgrade().is_some());
}
Rc::downgrade は Weak を作成します。Weak は weak count を増やすだけで、データを生存させません。使用する際には、upgrade() を呼び出して Option<Rc<T>> を取得します。データが既に解放されている場合、結果は None になります。drop(bob) の後、Customer の strong count が 0 になるため Customer は解放されます。order 内の Weak はまだ存在していても構いません。
注文 7 は Bob
strong=1, weak=1
顧客はまだいますか? false
一般的なルール: 「親 → 子」方向は Rc を、「子 → 親」方向は Weak を使用します。
5. Arc<T>: 複数スレッド間での複数のオーナー
5.1. なぜ Rc はスレッドで使えないのか
Rc は通常の加算でカウンターを更新するため、複数のスレッドが同時に操作する場合は安全ではありません。そのため、Rc は trait Send を実装しません。Rc を thread::spawn に渡そうとすると、コンパイラは拒否します:
error[E0277]: `Rc<Customer>` cannot be sent between threads safely
これは Rust の強みです: データレースエラーはコンパイル時にブロックされます。Arc (Atomically Reference Counted) は Rc と全く同じ API を持っていますが、カウンターの更新に atomic 操作を使用します。
5.2. 読み取り専用データの共有
use std::sync::Arc;
use std::thread;
struct ShopConfig {
currency: String,
vat_percent: u64,
}
fn main() {
let config = Arc::new(ShopConfig { currency: "VND".into(), vat_percent: 10 });
let handles: Vec<_> = [100_000u64, 200_000]
.into_iter()
.map(|price| {
let config = Arc::clone(&config);
thread::spawn(move || {
let vat = price * config.vat_percent / 100;
println!("{} + 税 = {} {}", price, price + vat, config.currency);
})
})
.collect();
for h in handles {
h.join().unwrap();
}
}
各スレッドは move を通じて独自の Arc コピーを受け取ります。すべてのコピーは同じ ShopConfig を指しています。データは読み取り専用なので、ロックは不要です。2 つの出力行の順序は実行ごとに異なる可能性があります。
100000 + 税 = 110000 VND
200000 + 税 = 220000 VND
5.3. Arc<Mutex<T>> でデータを変更する
RefCell はスレッド間では安全ではありません。マルチスレッド対応の同等物は Mutex (または RwLock) です:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let revenue = Arc::new(Mutex::new(0u64));
let mut handles = Vec::new();
for amount in [100u64, 250, 400] {
let revenue = Arc::clone(&revenue);
handles.push(thread::spawn(move || {
*revenue.lock().unwrap() += amount;
}));
}
for h in handles {
h.join().unwrap();
}
println!("売上: {}", *revenue.lock().unwrap());
}
lock() は MutexGuard を返します。ガードが drop されるときにロックは自動的に解放されます。ここではステートメント終了時に解放されます。unwrap() は mutex が poisoned (別のスレッドがロック保持中に panic した) 場合を処理します。
売上: 750
注意: 単純な整数カウンターの場合、
Arc<AtomicU64>はArc<Mutex<u64>>より高速です。また、単一スレッドのみの場所ではArcを使用しないでください。atomic 操作は通常の加算より高コストですが、差は大きくありません。
6. 参照カウント動作のテストを書く
Rust には組み込みのテストフレームワークがあり、追加の crate は不要です。次のテストを src/lib.rs または src/main.rs の末尾に配置します:
#[cfg(test)]
mod tests {
use std::rc::{Rc, Weak};
use std::sync::{Arc, Mutex};
use std::thread;
struct Customer {
name: String,
}
#[test]
fn rc_clone_increases_strong_count_and_drop_decreases_it() {
let alice = Rc::new(Customer { name: "Alice".into() });
let copy = Rc::clone(&alice);
assert_eq!(Rc::strong_count(&alice), 2);
assert!(Rc::ptr_eq(&alice, ©));
drop(copy);
assert_eq!(Rc::strong_count(&alice), 1);
assert_eq!(alice.name, "Alice");
}
#[test]
fn weak_upgrade_returns_none_after_owner_dropped() {
let bob = Rc::new(Customer { name: "Bob".into() });
let weak: Weak<Customer> = Rc::downgrade(&bob);
assert!(weak.upgrade().is_some());
drop(bob);
assert!(weak.upgrade().is_none());
}
#[test]
fn arc_mutex_sums_correctly_across_threads() {
let revenue = Arc::new(Mutex::new(0u64));
let handles: Vec<_> = (1..=100)
.map(|i| {
let revenue = Arc::clone(&revenue);
thread::spawn(move || *revenue.lock().unwrap() += i)
})
.collect();
handles.into_iter().for_each(|h| h.join().unwrap());
assert_eq!(*revenue.lock().unwrap(), 5050);
assert_eq!(Arc::strong_count(&revenue), 1);
}
}
最初のテストはカウンターをチェックし、Rc::ptr_eq を使用して 2 つの Rc が同じメモリ領域を指していることを確認します。つまり、deep copy はありません。2 番目のテストは Weak がデータを生存させないことを確認します。3 番目のテストは 100 個のスレッドを実行して合計し、結果は 5050 である必要があります。すべてのスレッドが join した後、strong reference は 1 つだけです。
テストを実行:
cargo test
running 3 tests
test tests::rc_clone_increases_strong_count_and_drop_decreases_it ... ok
test tests::weak_upgrade_returns_none_after_owner_dropped ... ok
test tests::arc_mutex_sums_correctly_across_threads ... ok
test result: ok. 3 passed; 0 failed
7. どの型を選ぶか?
| ニーズ | 選択肢 |
|---|---|
| 1 つのオーナー、ヒープが必要 (再帰的な型、トレイトオブジェクト、大きなデータのための低コスト移動) | Box<T> |
| 複数のオーナー、1 つのスレッド、読み取り専用 | Rc<T> |
| 複数のオーナー、1 つのスレッド、変更が必要 | Rc<RefCell<T>> |
| 複数のオーナー、複数のスレッド、読み取り専用 | Arc<T> |
| 複数のオーナー、複数のスレッド、変更が必要 | Arc<Mutex<T>> / Arc<RwLock<T>> |
| 逆参照、キャッシュ、循環を回避 | Weak<T> |
いくつかの一般的なエラー:
- borrow だけで十分な場合にスマートポインタを使用する。 関数がデータを読み込むだけの場合は、
Rc<Customer>の代わりに&Customerを受け取ります。こうすると関数がより柔軟になり、カウンターを増減する必要がなくなります。 Rc<RefCell<T>>の過度な使用。 このパターンが多く出現するコードは、通常 Java や C# からの OOP 思考を持ち込んでいることを示しています。所有権を再構成するか、Vec内のインデックスを使用することを検討してください (arena パターン)。- 循環参照。
RcとArcは、2 つのオブジェクトが strong reference で相互に指し合うとメモリリークが発生する可能性があります。一方向にWeakを使用してください。 MutexGuardを長く保持する。 特に async コードで.await経由でガードを保持する場合、または異なるスレッドで異なる順序で複数の mutex をロックする場合、デッドロックが発生する可能性があります。
8. まとめ
Box<T> は、単一のオーナーでヒープにデータを配置する必要がある場合に使用します。典型的には再帰的な型とトレイトオブジェクトです。Rc<T> は単一スレッド内で複数のオーナーを許可し、Arc<T> は atomic カウンターのおかげで複数スレッド間で同じことを行います。スマートポインタ自体は読み込みだけを許可します。データを変更する必要がある場合は、RefCell (単一スレッド) または Mutex/RwLock (複数スレッド) と組み合わせ、循環参照を避けるために Weak を使用します。通常の borrow から始めて、デフォルトの所有権ルールが本当に対応できなくなったときだけスマートポインタに切り替えてください。