Rustのスマートポインタ:Box、Rc、RefCell、および内部の可変性
スマートポインタとは、「追加の挙動を持つデータ構造」のことです。ポインタと同様にメモリを参照しますが、自動メモリ管理、参照カウント、借用チェックなど、通常のポインタにはない機能を提供します。
通常のポインタを「住所が書かれた一枚の紙」に例えるなら、スマートポインタは「専属の警備員、清掃員、台帳を備えた不動産管理システム」のようなものです。Rustのスマートポインタは、コンパイル時に安全性を保証し、実行時のオーバーヘッドは(参照カウントによる最小限のオーバーヘッドを除けば)一切ありません。
1. 学習内容
Box<T>ヒープの割り当てと所有権の移転Derefトレイト — 参照演算子のオーバーロードDropトレイト—デストラクタによる自動クリーンアップRc<T>参照カウント — シングルスレッドのマルチオーナー共有RefCell<T>内部のばらつき—実行時の借用チェックRc<RefCell<T>>組み合わせモード—共有+編集可能
2. シェアデスクの物語
(1) 問題:複数のユーザーが同時に同じ文書を閲覧・編集したい
月曜日の朝、ルナの会社では「シェアデスク」制度が導入された。5人のチームに対して、デスクはたったの3つしかなかった。
- アリスはプロジェクトの提案書(PDF)を確認する必要がありますが、自分で印刷するのは避けたいと考えています。他の誰かが持っているものを借りたいのです。
- ボブも同じ提案書を確認し、それにメモを書き込む必要があります。
- キャロルも同時に閲覧したいのですが、編集はせず、閲覧のみしたいとのことです。
- 問題は、アリスがその文書を読み終えて破棄したにもかかわらず、ボブとキャロルがまだそれを読んでいる場合、そこで問題が発生してしまうということです。
- さらに悪いことに、ボブが変更を加えている間に、アリスも変更を加えていたため、ページがめちゃくちゃになってしまった。
「この文書を今なお閲覧している人が何人いるかさえ分かれば、誰も閲覧しなくなったらすぐに破棄できるのに……」 「誰かが編集中のときは、他の誰も変更できないようにできればいいのに……」
(2) 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) スマートポインターシステム
graph TB
A[Rust Smart Pointers] --> B[Box<T> Heap Allocation]
A --> C[Rc<T> Reference Count]
A --> D[RefCell<T> 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<RefCell<T>>"]
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により、リソースは自動的に解放されるため、手動でfreeやdeleteを行う必要はありません。
4. スマートポインタの例
(1) ▶ サンプル:Box—ヒープ割り当てと再帰型 (難易度 ⭐)
// ============================================
// 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);
}
出力:
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 — 共有所有権による参照カウント (難易度 ⭐⭐)
// ============================================
// 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
}
出力:
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—内部変動(難易度 ⭐⭐)
// ============================================
// 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.");
}
出力:
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>>—共有 + 可変 (難易度 ⭐⭐⭐)
// ============================================
// 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
}
出力:
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) ▶ サンプル:総合演習—グラフデータ構造(難易度 ⭐⭐⭐)
// ============================================
// 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));
}
出力:
=== 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()はその弱参照を強参照にアップグレードしようとします。
❓ よくある質問
Box<T>と通常の参照&Tの違いは何ですか?Box<T>はデータを所有していますが、&Tは単にデータを借りているだけです。Rc<T> と Arc<T> の違いは何ですか?Rc<T> はシングルスレッドの参照カウントであり、Arc<T> はマルチスレッドの原子的な参照カウントです。RefCell<T> と Cell<T> の違いは何ですか?Cell<T> は値のコピー(または移動)によって内部の可変性を実現していますが、RefCell<T> は参照によって実現しています。&mut T の代わりに RefCell<T> を使用すべき場合はいつですか?RefCell を使用します。Rc<RefCell<T>> と &mut T のパフォーマンスの違いは何ですか?Rc<RefCell<T>> では、参照カウントや実行時の借用チェックによるオーバーヘッドが発生しますが、&mut T ではオーバーヘッドはゼロです。📖 まとめ
Box<T>は、再帰型、大規模なデータセット、およびトレイトオブジェクトに使用されるヒープ割り当て型のスマートポインタであり、追加のオーバーヘッドを一切生じさせることなく、一意の所有権を保証します。Derefトレイト により、スマートポインタが*参照解除演算子をサポートできるようになり、Dropトレイトにより、スコープが終了した際にリソースが自動的にクリーンアップされるようになりますRc<T>は、シングルスレッド環境での共有所有権を可能にする参照カウント付きスマートポインタです。一方、Rc::cloneはデータをコピーすることなく、カウントのみをインクリメントします。RefCell<T>内部的な可変性を提供し、借用チェックをコンパイル時から実行時に先送りする。ルール違反が発生するとパニックが発生するCell<T>値のコピーを用いて内部の可変性を実装する(T: Copyが必要);RefCell<T>は参照を用いて実装する(任意の型が許可される)Rc<RefCell<T>>組み合わせモード(共有所有権+内部可変性)は、Rustのシングルスレッドプログラミングにおいて最も一般的に使用される柔軟なデータ共有ソリューションです。
📝 練習問題
- 難易度 ⭐:
Box<T>を使用して、再帰的に(少なくとも 3 つの要素を含む)Cons Listを作成し、そのリスト全体を出力するプログラムを作成してください。リストの出力には#[derive(Debug)]を使用する必要があります。 - 難易度 ⭐⭐:「共有ノート」システムを実装する:
Rc<RefCell<String>>を使用して、3人(アリス、ボブ、キャロル)が1つのノート文字列を共有できるようにする。全員がその内容を読み取ることができ、全員が書き込み(内容の追加)を行うことができる。全員が書き込みを終えた後、全員が完全な内容を確認できることを検証する。 - 難易度 ⭐⭐⭐:「グラフオブザーバー」パターンを実装してください。
trait Observer { fn notify(&self, msg: &str); }を定義し、次にLoggerObserverを実装してください(LoggerObserverは内部でRefCell<Vec<String>>を使用してメッセージをログに記録します)。次に、Rc<RefCell<dyn Observer>>を使用して、同じサブジェクトから複数のオブザーバーに通知が行われるようにします。main関数内で 2 つのオブザーバーを作成し、サブジェクトから 3 つのメッセージを送信させ、両方のオブザーバーがすべてのメッセージを受信したことを確認してください。



