15. スマートポインタ

🎯 この章のゴール

  • Box<T> でヒープに値を置く
  • Rc<T> で 1 つの値を複数で所有する
  • RefCell<T> で「内部可変性」を使う

💡 ひとことで言うと

スマートポインタは おまけ機能付きのポインタ。Box は「ヒープに置く」、Rc は「みんなで持つ(参照カウント)」、RefCell は「借用チェックを実行時に回す」機能付きです。

早見表

型所有者可変性借用チェック用途
Box<T>1 つmut なら可コンパイル時ヒープ確保、再帰型、トレイトオブジェクト
Rc<T>複数不変のみコンパイル時グラフ・共有データ
RefCell<T>1 つ内部可変実行時&self から書き換えたい

Box<T>: ヒープに置く

#[derive(Debug)]
enum List {
    Cons(i32, Box<List>), // Box がないと「サイズが無限」でコンパイルエラー
    Nil,
}

use List::{Cons, Nil};

fn main() {
    let b = Box::new(5);
    println!("b = {}", *b + 1);  // * で中身を取り出す(Deref)

    let list = Cons(1, Box::new(Cons(2, Box::new(Nil))));
    println!("{list:?}");
}
Playground で開く ↗

Box は Deref トレイトで普通の参照のように使え、Drop トレイトでスコープを抜けると中身も解放されます。

Rc<T>: 参照カウントで共有

use std::rc::Rc;

fn main() {
    let shared = Rc::new(String::from("みんなのデータ"));
    let a = Rc::clone(&shared); // 中身はコピーせず、カウントを +1
    {
        let b = Rc::clone(&shared);
        println!("count = {}", Rc::strong_count(&shared)); // 3
        println!("{b}");
    }
    println!("count = {}", Rc::strong_count(&shared));     // 2
    println!("{a}");
}
Playground で開く ↗

カウントが 0 になったときに値が解放されます。Rc は シングルスレッド専用 です(マルチスレッドは 16 章の Arc)。

RefCell<T>: 内部可変性

不変な参照しか持っていなくても、中身を書き換えられるようにします。借用ルールは 実行時 にチェックされます。

use std::cell::RefCell;
use std::rc::Rc;

#[derive(Debug)]
struct Account {
    balance: RefCell<i32>,
}

fn main() {
    let acc = Rc::new(Account { balance: RefCell::new(100) });
    let alice = Rc::clone(&acc);
    let bob = Rc::clone(&acc);

    *alice.balance.borrow_mut() += 50;  // 共有しているのに書き換えられる!
    *bob.balance.borrow_mut() -= 30;

    println!("残高: {}", acc.balance.borrow()); // 120
}
Playground で開く ↗

RefCell で borrow_mut() を同時に 2 つ取ると、コンパイルは通っても 実行時に panic します。コンパイル時チェックで済むなら、まずは普通の &mut を使いましょう。

Rc 同士がお互いを指す 循環参照 を作るとメモリリークします。親への参照のような「逆向き」のリンクには Weak<T>(Rc::downgrade)を使います。

🧪 やってみよう

下の書きかけコードの TODO を埋めて、▶ 実行 で確かめよう。

Rc<RefCell<Vec<String>>> で「共有 TODO リスト」を作り、2 つの変数から別々に push してみよう。

use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let todos = Rc::new(RefCell::new(Vec::<String>::new()));
    let alice = Rc::clone(&todos);
    let bob = Rc::clone(&todos);

    // TODO: alice と bob からそれぞれ borrow_mut().push(...) しよう

    println!("{:?}", todos.borrow());
}
Playground で開く ↗
💡 解答例を見る
use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let todos = Rc::new(RefCell::new(Vec::<String>::new()));
    let alice = Rc::clone(&todos);
    let bob = Rc::clone(&todos);

    alice.borrow_mut().push(String::from("牛乳を買う"));
    bob.borrow_mut().push(String::from("部屋を掃除する"));

    println!("{:?}", todos.borrow());
}
Playground で開く ↗

📝 まとめ

  • Box<T>: ヒープ確保。再帰型やトレイトオブジェクトに
  • Rc<T>: 参照カウントで複数所有(シングルスレッド)
  • RefCell<T>: 実行時借用チェックで内部可変性。Rc<RefCell<T>> が定番

✅ 理解度クイズ

Q1
再帰的なデータ構造(リストや木)で Box<T> が必要になる理由は?
Q2
次のコードで最後に表示される値は?
use std::rc::Rc;

fn main() {
    let a = Rc::new(5);
    let b = Rc::clone(&a);
    {
        let c = Rc::clone(&a);
    }
    println!("{}", Rc::strong_count(&a));
}
Q3
RefCell<T> で、borrow_mut() 中にさらに borrow_mut() するとどうなる?
Q4
複数の所有者がいて、かつ中身を書き換えたい(シングルスレッド)。定番の組み合わせは?