404 Not Found

404 Not Found


nginx

Rustのスマートポインタ:Box、Rc、RefCell、および内部の可変性

スマートポインタとは、「追加の挙動を持つデータ構造」のことです。ポインタと同様にメモリを参照しますが、自動メモリ管理、参照カウント、借用チェックなど、通常のポインタにはない機能を提供します。

通常のポインタを「住所が書かれた一枚の紙」に例えるなら、スマートポインタは「専属の警備員、清掃員、台帳を備えた不動産管理システム」のようなものです。Rustのスマートポインタは、コンパイル時に安全性を保証し、実行時のオーバーヘッドは(参照カウントによる最小限のオーバーヘッドを除けば)一切ありません。


1. 学習内容


2. シェアデスクの物語

(1) 問題:複数のユーザーが同時に同じ文書を閲覧・編集したい

月曜日の朝、ルナの会社では「シェアデスク」制度が導入された。5人のチームに対して、デスクはたったの3つしかなかった。

「この文書を今なお閲覧している人が何人いるかさえ分かれば、誰も閲覧しなくなったらすぐに破棄できるのに……」 「誰かが編集中のときは、他の誰も変更できないようにできればいいのに……」

(2) Rustのスマートポインタの解決策

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

fn main() {
    // Box: One person has exclusive access to a document,Destroy after reading
    let doc_box = Box::new(String::from("Project Proposal v1"));
    println!("Box Hold: {}", doc_box);
    // doc_box Automatically destroyed upon leaving the scope——No manual intervention required free

    // Rc: Shared Read-Only Access for Multiple Users
    let doc_rc = Rc::new(String::from("Shared Project Proposal"));
    let alice = Rc::clone(&doc_rc);
    let bob = Rc::clone(&doc_rc);
    println!("Reference Count: {}", Rc::strong_count(&doc_rc)); // 3

    // Rc<RefCell<T>>: Share + Variable
    let doc_shared = Rc::new(RefCell::new(String::from("Collaborative Documents")));
    let alice_view = Rc::clone(&doc_shared);
    let bob_view = Rc::clone(&doc_shared);

    // Bob Add annotations to a document(Needs to be revised)
    bob_view.borrow_mut().push_str("\nBob Comments on:Part 2 needs to be completed");
    // Alice View the documentation(Read-only)
    println!("Alice See: {}", alice_view.borrow());
    // Carol See the documentation as well(Read-only)
    println!("Carol See: {}", doc_shared.borrow());
}

Rustでは、「共有」および「変更」に関連するスレッドセーフ性の問題に対処するために、3種類のスマートポインタを使用しています。それらは、Box 排他所有権とヒープ割り当て、Rc 共有読み取り専用所有権、そしてRefCell 借用ルールの実行時チェックによって実現される内部可変性です。


3. 基本概念

(1) スマートポインターシステム

100%
graph TB
    A[Rust Smart Pointers] --> B[Box&lt;T&gt; Heap Allocation]
    A --> C[Rc&lt;T&gt; Reference Count]
    A --> D[RefCell&lt;T&gt; Internal Variability]

    B --> B1["let b = Box::new(42)"]
    B --> B2["Automatically leaves scope drop"]

    C --> C1["Rc::clone Increase the reference count"]
    C --> C2["strong_count == 0 Destroy on time"]
    C --> C3["Use in a single-threaded environment"]

    D --> D1["borrow() → Immutable References"]
    D --> D2["borrow_mut() → Variable References"]
    D --> D3["Runtime Check of Borrowing Rules"]

    A --> E[Combination Mode]
    E --> E1["Rc&lt;RefCell&lt;T&gt;&gt;"]
    E --> E2["Shared Ownership + Internal Variability"]

    A --> F[Core Trait]
    F --> F1["Deref: Dereference Operator *"]
    F --> F2["Drop: Destructor"]

(2) 3種類のスマートニードルの比較

プロパティ Box<T> Rc<T> RefCell<T>
所有権 単一所有 共有(参照カウント) 単一所有
可変性 可変(Box::new の後に *b = val が続く) 不変(読み取り専用の共有) 内部的に可変(実行時にチェックされる)
タイミングの確認 コンパイル時間 コンパイル時間 実行時間
パフォーマンス上のオーバーヘッド 追加のオーバーヘッドなし リファレンスカウントの変更(ごくわずか) 実行時の借用チェック(ごくわずか)
スレッドセーフ はい いいえ(シングルスレッド) いいえ(シングルスレッド)
ユースケース ヒープに割り当てられた大規模なデータセット、再帰型 マルチパスで共有される読み取り専用データ 不変参照の下でデータを変更する必要がある場合

(3) DerefおよびDropトレイト

特性 メソッド 関数
Deref fn deref(&self) -> &T カスタム型に対して *x の参照解除操作を適用できるようにする
Drop fn drop(&mut self) 値がスコープ外に出た際に自動的に呼び出されるクリーンアップロジック

(4) Boxモデル、Rcモデル、Arcモデルの比較

特徴 Box<T> Rc<T> Arc<T>
所有権 単一所有者 複数所有者(参照カウント) 複数所有者(アトミック参照カウント)
スレッドセーフ いいえ(シングルスレッド専用) いいえ(シングルスレッド専用) はい(スレッド間で共有可能)
参照カウント なし 非アトミック、オーバーヘッドが低い アトミック操作、オーバーヘッドが若干高い
変数へのアクセス &mut 直接変更可能 RefCell が必要 Mutex または RwLock が必要
代表的なシナリオ 再帰型 / ビッグデータのヒープ割り当て DAG / 共有読み取り専用データ マルチスレッドでのデータ共有
パフォーマンス 最速 平均 若干遅い(アトミック演算時)

Deref により、スマートポインタは通常の参照と同じように動作します。つまり、Box<T>&T と同じように使用できます。Drop により、リソースは自動的に解放されるため、手動で freedelete を行う必要はありません。


4. スマートポインタの例

(1) ▶ サンプル:Box—ヒープ割り当てと再帰型 (難易度 ⭐)

RUST
// ============================================
// Box<T> Three Typical Uses of:Heap Allocation、Recursive Types、Deref
// ============================================

// Recursive Types:Cons list(Must use Box,Because the size is unknown at compile time)
#[derive(Debug)]
enum List {
    Cons(i32, Box<List>),
    Nil,
}

use List::{Cons, Nil};

// Custom Smart Pointers (Demo Deref and Drop)
use std::ops::Deref;

struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &T {
        &self.0  // A reference to the internal value
    }
}

impl<T> Drop for MyBox<T> {
    fn drop(&mut self) {
        // In practical applications, resources are released here(For example, closing a file、Release the network connection)
        // println!("MyBox It was destroyed.~");
    }
}

fn main() {
    // --- 1. Box Basic Heap Allocation ---
    let b = Box::new(42);
    println!("Box the value in: {}", b);  // Automatic Dereferencing

    // --- 2. Recursive Types:Cons List ---
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
    println!("Recursive List: {:?}", list);

    // --- 3. Custom MyBox + Deref ---
    let my_box = MyBox::new(String::from("Hello"));
    // Deref lets &MyBox<String> automatically convert to &String, then to &str
    greet(&my_box);

    // --- 4. Dereference Operator ---
    let x = 10;
    let y = MyBox::new(x);
    assert_eq!(10, *y);  // *y Equivalent to *(y.deref())
    println!("*y == {}", *y);
}

fn greet(name: &str) {
    println!("Greeting: {}", name);
}

出力:

TEXT
Box the value in: 42
Recursive List: Cons(1, Cons(2, Cons(3, Nil)))
Greeting: Hello
*y == 10

Box<T> の主な利点は 2 つあります。1 つ目は、データをヒープ上に配置し(スタック上にはポインタのみを残す)、2 つ目は、コンパイル時にサイズが不明な型(再帰型など)を通常通り使用できるようにすることです。Deref トレイトにより、Box<T>&T と同様に間接参照できるようになります。これが、「スマートポインタ」の「スマート」という名の由来です。


(2) ▶ サンプル:Rc — 共有所有権による参照カウント (難易度 ⭐⭐)

RUST
// ============================================
// Rc<T> Reference Count:Multipath Sharing of Read-Only Data
// ============================================

use std::rc::Rc;

#[derive(Debug)]
enum BookList {
    Cons(String, Rc<BookList>),
    Nil,
}

use BookList::{Cons, Nil};

fn main() {
    // --- Scene:Three books are shared by two readers ---

    // Create a Basic Reading List
    let book_c = Rc::new(Cons("Rust Programming".to_string(),
        Rc::new(Cons("Introduction to Algorithms".to_string(),
            Rc::new(Cons("Design Patterns".to_string(),
                Rc::new(Nil))))));

    println!("Initial reference count: {}", Rc::strong_count(&book_c));

    // Alice Borrowed the reading list(Rc::clone Increase the reference count only,Do not deep-copy data)
    let alice = Rc::clone(&book_c);
    println!("Alice Reference Count After Borrowing: {}", Rc::strong_count(&book_c));

    {
        // Bob I also borrowed the reading list(In the inner scope)
        let bob = Rc::clone(&book_c);
        println!("Bob Reference Count After Borrowing: {}", Rc::strong_count(&book_c));

        // Both of them can read it.
        println!("Alice Book List: {:?}", *alice);
        println!("Bob Book List: {:?}", *bob);
    }  // Bob Out of scope,Decrease in reference count

    println!("Bob Citation Count After Returning the Book: {}", Rc::strong_count(&book_c));

    // Alice Still reading
    println!("Alice Still reading: {:?}", *alice);

    // All Rc After they have all gone out of scope,Only then is the data truly destroyed
    // Rc::strong_count == 0 When triggered drop
}

出力:

TEXT
Initial reference count: 1
Alice Reference Count After Borrowing: 2
Bob Reference Count After Borrowing: 3
Alice Book List: Cons("Rust Programming", Cons("Introduction to Algorithms", Cons("Design Patterns", Nil)))
Bob Book List: Cons("Rust Programming", Cons("Introduction to Algorithms", Cons("Design Patterns", Nil)))
Bob Citation Count After Returning the Book: 2
Alice Still reading: Cons("Rust Programming", Cons("Introduction to Algorithms", Cons("Design Patterns", Nil)))

Rc<T> は「参照カウント方式」を意味します。Rc::clone(&x) はデータのディープコピーを行わず、単に参照カウントを 1 増やします。データが破棄されるのは、すべての Rc ハンドルがスコープ外になり(参照カウントが 0 になった)、そのときのみです。Rc はシングルスレッド環境でのみ使用可能です。マルチスレッド環境では、Arc<T> を使用してください。


(3) ▶ サンプル:RefCell—内部変動(難易度 ⭐⭐)

RUST
// ============================================
// RefCell<T>:Runtime Borrowing Checks + Internal Variability
// ============================================

use std::cell::RefCell;

// Simulate a "Log Entry" Messenger
// External code can only obtain immutable references, but the log needs to be modified internally.
trait Messenger {
    fn send(&self, msg: &str);
}

// Logger:For internal use only RefCell Store Messages
struct Logger {
    // Even if Logger Inherently immutable,messages It can still be edited
    messages: RefCell<Vec<String>>,
}

impl Logger {
    fn new() -> Logger {
        Logger {
            messages: RefCell::new(Vec::new()),
        }
    }
}

impl Messenger for Logger {
    fn send(&self, msg: &str) {
        // borrow_mut() gets a mutable reference — even if self is &self
        self.messages.borrow_mut().push(msg.to_string());
    }
}

fn main() {
    let logger = Logger::new();

    // Calling via an immutable reference send——But it has actually been modified internally.
    logger.send("User Login");
    logger.send("Click the button");
    logger.send("Data submitted successfully");

    // borrow() Get an immutable reference
    let msgs = logger.messages.borrow();
    for (i, msg) in msgs.iter().enumerate() {
        println!("Log #{}: {}", i + 1, msg);
    }

    // --- Examples of Runtime Borrowing Violations(Cancel the Commentary Session panic)---
    // let mut_ref = logger.messages.borrow_mut();
    // let ref1 = logger.messages.borrow();    // panic! Both mutable and immutable references
    // println!("{}", ref1[0]);

    println!("RefCell The presentation is over.");
}

出力:

TEXT
Log #1: User Login
Log #2: Click the button
Log #3: Data submitted successfully
RefCell The presentation is over.

RefCell<T> の核心は「内部の可変性」にあります。たとえ Logger 自体が &self への不変参照であったとしても、RefCell を使用すれば内部データを変更することができます。違いは、借用ルールがコンパイル時ではなく実行時にチェックされる点にあります。ルールに違反した場合、プログラムはコンパイル時エラーを発生させるのではなく、パニックを起こします。


(4) ▶ サンプル:Rc<RefCell<T>>—共有 + 可変 (難易度 ⭐⭐⭐)

RUST
// ============================================
// Rc<RefCell<T>> Combination Mode:Shared Ownership + Internal Variability
// ============================================

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

// Simulate a"Shared Whiteboard"——Team members can write and read
#[derive(Debug)]
struct Whiteboard {
    content: RefCell<String>,
}

impl Whiteboard {
    fn new() -> Whiteboard {
        Whiteboard {
            content: RefCell::new(String::new()),
        }
    }

    fn write(&self, text: &str) {
        let mut content = self.content.borrow_mut();
        content.push_str(text);
        content.push('\n');
    }

    fn read(&self) -> String {
        self.content.borrow().clone()
    }
}

fn main() {
    // Create a shared whiteboard, wrapped with Rc so it can be held by multiple people
    let board = Rc::new(Whiteboard::new());

    // Alice and Bob both hold references to the whiteboard
    let alice_board = Rc::clone(&board);
    let bob_board = Rc::clone(&board);
    let carol_board = Rc::clone(&board);

    println!("Current reference count: {}", Rc::strong_count(&board));  // 4

    // Alice Write on the whiteboard
    alice_board.write("Alice: Today's Discussion Rust Smart Pointers");
    // Bob Additional Information
    bob_board.write("Bob: I'll explain the difference between Box and Rc");
    // Carol Write as well
    carol_board.write("Carol: RefCell Runtime borrowing checks are important");

    // Everyone can view the full content(Because they share the same RefCell)
    println!("=== Whiteboard Content ===");
    println!("{}", board.read());

    // Verify that all references point to the same data
    println!("Alice Length of content viewed: {}", alice_board.read().len());
    println!("Bob Length of content viewed: {}", bob_board.read().len());
    println!("Carol Length of content viewed: {}", carol_board.read().len());

    // board When leaving the scope,Reference Count Reset to Zero,Whiteboard Automatic Destruction
}

出力:

TEXT
Current reference count: 4
=== Whiteboard Content ===
Alice: Today's Discussion Rust Smart Pointers
Bob: I'll explain the difference between Box and Rc
Carol: RefCell Runtime borrowing checks are important

Alice Length of content viewed: 73
Bob Length of content viewed: 73
Carol Length of content viewed: 73

Rc<RefCell<T>> は、シングルスレッドの Rust プログラミングにおいて最も強力なコンポーザブルパターンの一つです。Rc は「複数の人が何かを保持したい」という問題を解決し、RefCell は「不変の参照を保持している人もそれを変更したい」という問題を解決します。Rcを「共有の図書館カード」に例えるなら、RefCellは「注釈を書き込める特別なコピー」と言えます。


(5) ▶ サンプル:総合演習—グラフデータ構造(難易度 ⭐⭐⭐)

RUST
// ============================================
// Comprehensive Example:Rc<RefCell<T>> Graph Node Implementation
// ============================================

use std::rc::{Rc, Weak};
use std::cell::RefCell;
use std::fmt;

struct Node {
    value: String,
    neighbors: Vec<Weak<RefCell<Node>>>,
}

impl Node {
    fn new(value: &str) -> Rc<RefCell<Node>> {
        Rc::new(RefCell::new(Node {
            value: value.to_string(),
            neighbors: Vec::new(),
        }))
    }

    fn add_neighbor(node: &Rc<RefCell<Node>>, neighbor: &Rc<RefCell<Node>>) {
        node.borrow_mut().neighbors.push(Rc::downgrade(neighbor));
    }
}

impl fmt::Debug for Node {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let neighbor_names: Vec<String> = self.neighbors.iter()
            .filter_map(|w| w.upgrade())
            .map(|n| n.borrow().value.clone())
            .collect();
        write!(f, "{} -> {:?}", self.value, neighbor_names)
    }
}

fn main() {
    let a = Node::new("A");
    let b = Node::new("B");
    let c = Node::new("C");
    let d = Node::new("D");

    Node::add_neighbor(&a, &b);
    Node::add_neighbor(&a, &c);
    Node::add_neighbor(&b, &c);
    Node::add_neighbor(&b, &d);
    Node::add_neighbor(&c, &d);

    println!("=== Graph Structure ===");
    for node in [&a, &b, &c, &d] {
        println!("{:?}", node.borrow());
    }

    println!("\n=== Nodes Reachable from Starting Point A ===");
    if let Some(neighbor) = a.borrow().neighbors.first().and_then(|w| w.upgrade()) {
        println!("A's first neighbor: {}", neighbor.borrow().value);
        let second_hop: Vec<String> = neighbor.borrow().neighbors.iter()
            .filter_map(|w| w.upgrade())
            .map(|n| n.borrow().value.clone())
            .collect();
        println!("Take it a step further from that neighbor: {:?}", second_hop);
    }

    println!("\n=== Strong reference counting ===");
    println!("A's Rc reference count: {}", Rc::strong_count(&a));
    let a_clone = Rc::clone(&a);
    println!("After clone, A's Rc reference count: {}", Rc::strong_count(&a));
    drop(a_clone);
    println!("After drop, A's Rc reference count: {}", Rc::strong_count(&a));
}

出力:

TEXT
=== Graph Structure ===
A -> ["B", "C"]
B -> ["C", "D"]
C -> ["D"]
D -> []

=== Nodes Reachable from Starting Point A ===
A's first neighbor: B
Take it a step further from that neighbor: ["C", "D"]

=== Strong reference counting ===
A's Rc reference count: 1
After clone, A's Rc reference count: 2
After drop, A's Rc reference count: 1

グラフのノードは、Rc<RefCell<Node>> を使用して共有所有権と内部的な可変性を実装しています。また、隣接関係では、Weak<RefCell<Node>> を使用して循環参照によるメモリリークを防止しています。Rc::downgrade は弱参照を作成し、weak.upgrade() はその弱参照を強参照にアップグレードしようとします。


❓ よくある質問

Q Box<T>と通常の参照&Tの違いは何ですか?
A Box<T>はデータを所有していますが、&Tは単にデータを借りているだけです。
Q Rc<T>Arc<T> の違いは何ですか?
A Rc<T> はシングルスレッドの参照カウントであり、Arc<T> はマルチスレッドの原子的な参照カウントです。
Q RefCell<T>Cell<T> の違いは何ですか?
A Cell<T> は値のコピー(または移動)によって内部の可変性を実現していますが、RefCell<T> は参照によって実現しています。
Q &mut T の代わりに RefCell<T> を使用すべき場合はいつですか?
A 不変の参照を保持しているものの、データを変更する必要がある場合に RefCell を使用します。
Q Rc<RefCell<T>>&mut T のパフォーマンスの違いは何ですか?
A Rc<RefCell<T>> では、参照カウントや実行時の借用チェックによるオーバーヘッドが発生しますが、&mut T ではオーバーヘッドはゼロです。

📖 まとめ


📝 練習問題

  1. 難易度 ⭐: Box<T> を使用して、再帰的に(少なくとも 3 つの要素を含む)Cons List を作成し、そのリスト全体を出力するプログラムを作成してください。リストの出力には #[derive(Debug)] を使用する必要があります。
  2. 難易度 ⭐⭐:「共有ノート」システムを実装する:Rc<RefCell<String>> を使用して、3人(アリス、ボブ、キャロル)が1つのノート文字列を共有できるようにする。全員がその内容を読み取ることができ、全員が書き込み(内容の追加)を行うことができる。全員が書き込みを終えた後、全員が完全な内容を確認できることを検証する。
  3. 難易度 ⭐⭐⭐:「グラフオブザーバー」パターンを実装してください。trait Observer { fn notify(&self, msg: &str); } を定義し、次に LoggerObserver を実装してください(LoggerObserver は内部で RefCell<Vec<String>> を使用してメッセージをログに記録します)。次に、Rc<RefCell<dyn Observer>> を使用して、同じサブジェクトから複数のオブザーバーに通知が行われるようにします。main 関数内で 2 つのオブザーバーを作成し、サブジェクトから 3 つのメッセージを送信させ、両方のオブザーバーがすべてのメッセージを受信したことを確認してください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%