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, &copy));

        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 から始めて、デフォルトの所有権ルールが本当に対応できなくなったときだけスマートポインタに切り替えてください。

rustsmart-pointerownershipconcurrencymemory