Rustのスマートポインタ徹底解説: Box、Rc、Arc、RefCell(2026年版)

RustのスマートポインタBox、Rc、Arc、RefCellを、2026年のコンパイル可能な例、判断表、頻出の面接質問とともに解説します。

RustのスマートポインタBox、Rc、Arc、RefCellによるメモリ管理の図

Rustのスマートポインタは、借用チェッカーだけでは表現できない所有権パターン、すなわちヒープ確保、共有所有権、そして共有参照を通じた変更を可能にするツールです。単なる参照(&T)が値を借用するだけなのに対し、スマートポインタは自身のデータを所有し、その上に追加の振る舞いを重ねます。本ガイドでは、Rust開発者が本番環境でも面接でも必ず出会う4つの型、BoxRcArcRefCellを、Rust 2024エディション向けのコンパイル可能な例とともに解説します。

1行でまとめると

単一所有者によるヒープ確保にはBox<T>、単一スレッドでの共有所有権にはRc<T>、スレッドをまたぐ共有所有権にはArc<T>、共有参照を通じて値を変更するにはRefCell<T>を使います。Rc<RefCell<T>>Arc<Mutex<T>>という組み合わせが、共有された可変状態をカバーします。

Rustにおけるスマートポインタとは

スマートポインタは、ポインタのように振る舞いながら追加のメタデータや機能を備えた構造体です。その多くはDerefトレイトを実装しており、*pointerやメソッド呼び出しがあたかも通常の参照であるかのように動作します。またDropトレイトも実装しているため、値がスコープを離れると後片付けが自動的に実行されます。標準ライブラリはここで扱う4つを提供しており、これらを理解する鍵は所有権と借用をしっかり把握することにあります。所有権と借用が、どの確保領域をいつ誰が解放するかを決めるからです。

参照との決定的な違いは所有権です。&Tは指し示すデータを決して所有しないため、その値より長く生きることはできません。スマートポインタはデータを所有し、そのライフタイムを制御し、決定的に解放します。スマートポインタに関するRust Bookの章がこれらを1つのカテゴリとして扱うのは、まさにこの「所有権に加えて振る舞いを持つ」という形を共有しているからです。

Box<T>: 再帰的な型やサイズ確定のためのヒープ確保

Box<T>は最もシンプルなスマートポインタです。値をヒープに格納し、スタック上にはそのポインタだけを保持します。所有者は1つだけで、確保そのもの以外にランタイムのオーバーヘッドはありません。最もよくある用途は、再帰的な型に既知のサイズを与えることです。自分自身を直接含む型は無限に大きくなってしまいますが、Boxは単なるポインタなので、指し示す対象に関係なくサイズが固定されます。

下の例は二分木を定義しています。各Nodeは2つの子を持ちますが、間接参照がなければコンパイラはTreeのサイズを計算できません。各子をBoxで包むことで、型レベルでの再帰を断ち切ります。

tree.rsrust
#[derive(Debug)]
enum Tree {
    Leaf(i32),
    // Box<Tree> puts each child on the heap, so Node has a fixed size (two pointers).
    Node(Box<Tree>, Box<Tree>),
}

fn sum(tree: &Tree) -> i32 {
    match tree {
        Tree::Leaf(value) => *value,                        // base case: return the leaf
        Tree::Node(left, right) => sum(left) + sum(right),  // recurse into both children
    }
}

fn main() {
    // Build (1) + ((2) + (3)) = 6
    let tree = Tree::Node(
        Box::new(Tree::Leaf(1)),
        Box::new(Tree::Node(
            Box::new(Tree::Leaf(2)),
            Box::new(Tree::Leaf(3)),
        )),
    );
    println!("sum = {}", sum(&tree)); // sum = 6
}

Boxは、大きな値のムーブがコストになる場合や、Box<dyn Error>のようなトレイトオブジェクトを返す場合にも重要です。いずれの場合も規則は同じで、所有者は1つ、そしてBoxがドロップされると自動的に解放されます。

Rc<T>: シングルスレッドコードでの共有所有権

ときには、複数の所有者を持ちながら、どれが最後に使われるのか明らかでない値が必要になります。Rc<T>(参照カウント)は、何人の所有者が存在するかを示す強参照カウントを保持することでこれを解決します。Rc::cloneは基となるデータをコピーせずにそのカウントを増やし、各ドロップがそれを減らします。カウントがゼロになると、値は解放されます。

複数のワーカーが読み取る設定オブジェクトを考えてみます。各ワーカーは必要な間だけ設定を保持すべきであり、確保領域は最後のワーカーがいなくなって初めて消えるべきです。

shared_config.rsrust
use std::rc::Rc;

#[derive(Debug)]
struct Config {
    endpoint: String,
    timeout_ms: u32,
}

fn main() {
    // Rc::new moves Config onto the heap with a strong count of 1.
    let config = Rc::new(Config {
        endpoint: "https://api.example.com".to_string(),
        timeout_ms: 5000,
    });

    // Rc::clone only bumps the reference count; it does not deep-copy Config.
    let worker_a = Rc::clone(&config);
    let worker_b = Rc::clone(&config);

    // All three handles point to the same allocation.
    println!("endpoint: {}", worker_a.endpoint);
    println!("timeout:  {}", worker_b.timeout_ms);

    // strong_count reports how many owners currently hold the value.
    println!("owners: {}", Rc::strong_count(&config)); // owners: 3
}

config.clone()ではなくRc::cloneを明示的に呼ぶのが慣用的です。この操作が安価であり、カウンタに触れるだけであることを読み手に伝えるからです。落とし穴は、Rc<T>が共有された不変の参照しか渡さないことです。共有する値を変更することはできず、スレッド間で送ることも安全ではありません。Rcのドキュメントが両方の制約を明記しています。

RefCell<T>とRustの内部可変性

Rustは通常、値が多数の不変参照を通じて共有されるか、あるいはちょうど1つの可変参照を通じて変更されるかのいずれかであることを強制し、これをコンパイル時に検査します。RefCell<T>は内部可変性を提供します。同じ検査をランタイムに移すことで、共有参照を通じて値を変更できるようにするのです。borrow()は共有の読み取りガードを返し、borrow_mut()は排他的な書き込みガードを返します。規則は同一ですが、違反はコンパイルに失敗する代わりにパニックを起こします。

Rcと組み合わせると、シングルスレッドにおける共有可変の定石パターンRc<RefCell<T>>が生まれます。同じ値をすべて更新できる複数の所有者です。

counter.rsrust
use std::rc::Rc;
use std::cell::RefCell;

// Rc gives shared ownership; RefCell allows mutation through a shared reference.
type SharedCounter = Rc<RefCell<u32>>;

fn increment(counter: &SharedCounter) {
    // borrow_mut() hands out an exclusive reference, checked at runtime.
    *counter.borrow_mut() += 1;
}

fn main() {
    let counter: SharedCounter = Rc::new(RefCell::new(0));

    let handle_a = Rc::clone(&counter);
    let handle_b = Rc::clone(&counter);

    increment(&handle_a);
    increment(&handle_b);
    increment(&counter);

    // borrow() gives a shared read guard; the value is now 3.
    println!("count = {}", counter.borrow()); // count = 3
}
RefCellは借用チェックをランタイムに移す

別のborrow()またはborrow_mut()ガードがまだ生きている間にborrow_mut()を呼ぶと、already borrowed: BorrowMutErrorでパニックします。安全性の保証は保たれますが、コンパイラなら捕捉していたはずのロジックのバグがランタイムのクラッシュになってしまいます。ガードは短命に保ち、同じRefCellに再び入り込む可能性のある関数呼び出しをまたいで借用を保持することは避けるべきです。

内部可変性は参照サイクルが潜む場所でもあります。互いを指し示す2つのRc<RefCell<T>>の値は、互いの強参照カウントを永遠にゼロより上に保ち続けるため、どちらも決して解放されません。解決策はWeak<T>です。これは強参照カウントに影響しない非所有のハンドルです。定番の解説はLearn Rust With Entirely Too Many Linked Listsにあります。

Arc<T>: 並行処理のためのスレッドセーフな参照カウント

Arc<T>(アトミック参照カウント)はRc<T>のマルチスレッド版の兄弟です。公開APIはほぼ同一ですが、カウンタがアトミック操作を使うため、複数のスレッドから同時にArcをクローンしたりドロップしたりしても正しく動作します。このアトミック性こそが、共有がスレッド境界を越えるときにのみArcが選ばれる理由です。アトミックなインクリメントは、Rcが使う通常のものより測定可能なほど遅いのです。

Arc<T>も依然として不変参照しか渡さないため、スレッドをまたいだ変更には同期プリミティブが必要です。通常のパートナーはMutex<T>であり、並行コードにおいてRc<RefCell<T>>に対応するArc<Mutex<T>>パターンを与えます。下の例では、共有された合計をそれぞれ1000回ずつインクリメントする4つのスレッドを生成します。

parallel_sum.rsrust
use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    // Arc is safe to share across threads; Mutex serializes access to the inner value.
    let total = Arc::new(Mutex::new(0u64));
    let mut handles = Vec::new();

    for _ in 0..4 {
        // Each thread gets its own Arc handle (an atomic count bump).
        let total = Arc::clone(&total);
        handles.push(thread::spawn(move || {
            for _ in 0..1000 {
                // lock() blocks until the mutex is free, then returns a guard.
                let mut value = total.lock().unwrap();
                *value += 1;
            }
        }));
    }

    for handle in handles {
        handle.join().unwrap(); // wait for every thread before reading
    }

    println!("total = {}", *total.lock().unwrap()); // total = 4000
}

コンパイラが境界を代わりに強制してくれます。Rc<T>Sendではないため、thread::spawnにムーブしようとするとコンパイルに失敗し、Arcへと導かれます。シングルスレッド内でArcに手を伸ばすと、決して使わないアトミック操作の代償を払うだけです。Arcのドキュメントがメモリ順序の保証を詳しく扱っており、Arc<Mutex<T>>Tokioを用いた非同期Rustにおける共有状態の多くを支えています。

Rustの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

Box、Rc、Arc、RefCell: それぞれの使いどころ

4つの型は2つの軸で組み合わさります。値が何人の所有者を持つか、そして共有ハンドルを通じて変更できるかどうかです。次の表がトレードオフをまとめています。

| 型 | 所有権 | 共有ハンドル経由の変更 | スレッドセーフ | オーバーヘッド | |------|-----------|----------------------------|-------------|----------| | Box<T> | 単一 | 不可 | T: Sendなら可 | ヒープ確保のみ | | Rc<T> | 共有 | 不可 | 不可 | 非アトミックなカウント | | Arc<T> | 共有 | 不可 | 可 | アトミックなカウント | | RefCell<T> | 単一 | 可(ランタイム検査) | 不可 | ランタイムの借用フラグ |

実践的な決定木は短いものです。ヒープ上に所有者が1つ必要ならBox。1つのスレッドで多数の所有者が必要ならRc。スレッドをまたいで多数の所有者が必要ならArc。共有された値を変更する必要があるなら、内側の型をRefCell(シングルスレッド)またはMutex(マルチスレッド)で包みます。参照とポインタ型に関するEffective Rustの指針も同じ階層構造にたどり着いています。最も権限の小さい選択肢から始め、コンパイラが強制したときにのみ機能を追加していきます。

Rustスマートポインタの面接質問

スマートポインタは信頼できる面接トピックです。候補者が構文だけでなく所有権を理解しているかを試すからです。以下の質問は頻出であり、さらに多くはRustスマートポインタ面接モジュールに集められています。

RcArcの違いは何ですか。 どちらも参照カウントを通じて共有所有権を提供します。Rcは非アトミックなカウンタを使い、単一スレッドに限定されます。Arcはアトミック操作を使い、わずかなランタイムコストと引き換えにスレッド間で共有できます。コンパイラがこの区分を強制します。RcSendでもSyncでもないため、スレッド境界を越えられません。

なぜRcRefCellを組み合わせるのですか。 Rcは共有所有権を与えますが、アクセスは不変に限られます。RefCellは内部可変性を追加し、ランタイムの借用チェックとともに、所有者が共有参照を通じて値を変更できるようにします。両者を合わせたRc<RefCell<T>>が、シングルスレッドにおける共有可変の慣用的な構成要素です。

Rustで参照カウントはどのようにメモリをリークさせ得ますか。 互いを参照する2つのRc(またはArc)の値はサイクルを形成し、その強参照カウントは決してゼロに達しないため、確保領域は解放されません。非所有の参照を保持するWeak<T>でサイクルを断ち切ると、正しい後片付けが回復します。

BoxRcより好ましいのはどのような場合ですか。 単一の所有者で十分なときは常にそうです。Boxには参照カウントのオーバーヘッドがないため、ヒープ確保、再帰的な型、トレイトオブジェクトのデフォルトとなります。真の共有所有権が必要なときにのみRcに手を伸ばします。

まとめ

Rustのスマートポインタは、所有権を制約から組み合わせ可能な構成要素の集合へと変えます。どれを選ぶかは、問題の形から直接導かれます。

  • 値がヒープ上に単一の所有者を必要とするとき、再帰的な型がサイズの固定を必要とするとき、あるいは関数がトレイトオブジェクトを返すときはBox<T>を選びます。
  • 1つのスレッド上で複数の所有者にはRc<T>を使い、所有権がスレッド境界を越えた瞬間にArc<T>へ切り替えます。
  • シングルスレッドコードでの内部可変性にはRefCell<T>を、その並行版にはMutex<T>を加え、借用ガードとロックガードは短命に保ちます。
  • 意図的に組み合わせます。1つのスレッド上の共有可変状態にはRc<RefCell<T>>、スレッドをまたぐ場合にはArc<Mutex<T>>です。
  • カウント付きポインタ同士の参照サイクルに注意し、リークを避けるためにWeak<T>で断ち切ります。
  • 最も権限の小さい型をデフォルトとし、より多くの機能が必要になったときはコンパイラに教えてもらいます。

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

タグ

#rust
#smart-pointers
#memory-management
#interview
#rust-2024-edition

共有

関連記事