8. Smart Pointers and Interior Mutability 🟡
# 9. 智能指针与内部可变性 🟡
What you’ll learn:
本章将学到什么:
- Box, Rc, Arc for heap allocation and shared ownership
如何使用Box、Rc、Arc做堆分配与共享所有权- Weak references for breaking Rc/Arc reference cycles
如何用Weak打破Rc、Arc的引用环- Cell, RefCell, and Cow for interior mutability patterns
如何用Cell、RefCell和Cow组织内部可变性模式- Pin for self-referential types and ManuallyDrop for lifecycle control
如何用Pin处理自引用类型,以及如何用ManuallyDrop控制生命周期
Box, Rc, Arc — Heap Allocation and Sharing
Box、Rc、Arc:堆分配与共享
#![allow(unused)]
fn main() {
// --- Box<T>: Single owner, heap allocation ---
let boxed: Box<i32> = Box::new(42);
println!("{}", *boxed);
enum List<T> {
Cons(T, Box<List<T>>),
Nil,
}
let writer: Box<dyn std::io::Write> = Box::new(std::io::stdout());
// --- Rc<T>: Multiple owners, single-threaded ---
use std::rc::Rc;
let a = Rc::new(vec![1, 2, 3]);
let b = Rc::clone(&a);
let c = Rc::clone(&a);
println!("Ref count: {}", Rc::strong_count(&a));
// --- Arc<T>: Multiple owners, thread-safe ---
use std::sync::Arc;
let shared = Arc::new(String::from("shared data"));
let handles: Vec<_> = (0..5).map(|_| {
let shared = Arc::clone(&shared);
std::thread::spawn(move || println!("{shared}"))
}).collect();
for h in handles { h.join().unwrap(); }
}
Box<T> is the simplest smart pointer: one owner, heap storage, no reference counting. Rc<T> adds shared ownership inside a single thread. Arc<T> does the same thing with atomic reference counting so it is safe to share across threads.Box<T> 是最朴素的智能指针:单一所有者、数据放堆上、没有引用计数。Rc<T> 在单线程里提供共享所有权。Arc<T> 则把这个能力扩展到多线程,通过原子引用计数保证线程安全。
Weak References — Breaking Reference Cycles
弱引用:打破引用环
Rc and Arc rely on reference counting, so they cannot reclaim cycles by themselves. Weak<T> is the non-owning counterpart used for back-references, caches, and parent pointers.Rc 和 Arc 靠的是引用计数,所以它们没法自己回收环。Weak<T> 就是它们的非拥有版本,专门拿来做回指、缓存和父指针这类关系。
#![allow(unused)]
fn main() {
use std::rc::{Rc, Weak};
use std::cell::RefCell;
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
}
Rule of thumb: Ownership edges use Rc or Arc; back-edges and observational references use Weak.
经验法则:真正拥有对象的边,用 Rc 或 Arc;回指关系、观察性引用和缓存句柄,用 Weak。
Cell and RefCell — Interior Mutability
Cell 与 RefCell:内部可变性
Sometimes code needs to mutate state through &self. Rust normally forbids that, so the standard library offers interior mutability wrappers that move the checking strategy from compile time to runtime or to simple copy-based operations.
有时候代码确实需要在拿着 &self 的情况下修改状态。Rust 平常会禁止这种事,所以标准库专门提供了内部可变性包装器,把检查方式从纯编译期规则,切换成运行时检查或简单的拷贝式更新。
#![allow(unused)]
fn main() {
use std::cell::{Cell, RefCell};
struct Counter {
count: Cell<u32>,
}
impl Counter {
fn new() -> Self { Counter { count: Cell::new(0) } }
fn increment(&self) {
self.count.set(self.count.get() + 1);
}
}
struct Cache {
data: RefCell<Vec<String>>,
}
}
Cell<T> works best for Copy data or swap-style updates. RefCell<T> works for any type, but borrow rules are enforced at runtime, which means violations become panics instead of compiler errors.Cell<T> 最适合 Copy 数据,或者那种整体替换值的场景。RefCell<T> 对任意类型都能用,但借用规则变成了运行时检查,因此一旦违反规则,代价就是 panic,而不是编译期报错。
Cell vs RefCell:
Cellnever panics from borrowing because it does not hand out references; it just copies or swaps values.RefCellcan panic if immutable and mutable borrows overlap at runtime.Cell和RefCell的区别:Cell不会因为借用规则而 panic,因为它根本不把引用交出去,它只是在内部做复制或替换。RefCell会把引用借出来,所以一旦可变借用和不可变借用在运行时冲突,就会 panic。
Cow — Clone on Write
Cow:写时克隆
Cow stores either borrowed data or owned data, and it only clones when mutation becomes necessary.Cow 可以存借来的数据,也可以存自己拥有的数据,而且只有在确实需要修改时才会触发克隆。
#![allow(unused)]
fn main() {
use std::borrow::Cow;
fn normalize(input: &str) -> Cow<'_, str> {
if input.contains('\t') {
Cow::Owned(input.replace('\t', " "))
} else {
Cow::Borrowed(input)
}
}
}
This is great for hot paths where most inputs already satisfy the desired format, but a few need cleanup.
它特别适合那种“绝大多数输入本来就合格,只有少量输入需要额外修正”的热点路径。
Cow<'_, [u8]> for Binary Data
二进制数据里的 Cow<'_, [u8]>
The same idea works for byte buffers:
同样的思路也很适合字节缓冲区:
#![allow(unused)]
fn main() {
use std::borrow::Cow;
fn pad_frame(frame: &[u8], min_len: usize) -> Cow<'_, [u8]> {
if frame.len() >= min_len {
Cow::Borrowed(frame)
} else {
let mut padded = frame.to_vec();
padded.resize(min_len, 0x00);
Cow::Owned(padded)
}
}
}
When to Use Which Pointer
各种指针什么时候用
| Pointer 指针 | Owner Count 所有者数量 | Thread-Safe 线程安全 | Mutability 可变性 | Use When 适用场景 |
|---|---|---|---|---|
Box<T> | 1 | ✅(if T: Send) | Via &mut | Heap allocation, trait objects, recursive types 堆分配、trait object、递归类型 |
Rc<T> | N | ❌ | None by itself | Shared ownership in one thread 单线程共享所有权 |
Arc<T> | N | ✅ | None by itself | Shared ownership across threads 多线程共享所有权 |
Cell<T> | — | ❌ | .get() / .set() | Interior mutability for Copy typesCopy 类型的内部可变性 |
RefCell<T> | — | ❌ | Runtime borrow checking | Interior mutability for arbitrary single-threaded data 单线程任意类型的内部可变性 |
Cow<'_, T> | 0 or 1 | ✅(if T: Send) | Clone on write | Avoid allocation when mutation is rare 修改不常发生时减少分配 |
Pin and Self-Referential Types
Pin 与自引用类型
Pin<P> exists to promise that a value will not be moved after it has been pinned. That is essential for self-referential structs and for async futures that may store references into their own state machines.Pin<P> 的意义是:一旦值被 pin 住,就承诺之后不再移动它。这对于自引用结构体,以及那些会把引用存进自身状态机里的 async future,都是关键前提。
#![allow(unused)]
fn main() {
use std::pin::Pin;
use std::marker::PhantomPinned;
struct SelfRef {
data: String,
ptr: *const String,
_pin: PhantomPinned,
}
}
Key concepts:
关键概念:
| Concept 概念 | Meaning 含义 |
|---|---|
Unpin | Moving this type is safe 移动它是安全的 |
!Unpin / PhantomPinned | This type must stay put 这个类型必须保持地址稳定 |
Pin<&mut T> | Mutable access without moving 可变访问,但不能移动 |
Pin<Box<T>> | Heap-pinned owned value 固定在堆上的拥有型值 |
Most application code does not touch Pin directly because async runtimes handle it. It mainly matters when implementing futures manually or designing low-level self-referential abstractions.
多数业务代码其实碰不到 Pin,因为 async 运行时已经把这件事代劳了。它主要在手写 future,或者设计底层自引用抽象时才会真正跳到台前。
Pin Projections — Structural Pinning
Pin 投影:结构性固定
Once a whole struct is pinned, accessing its fields becomes subtle. Some fields are logically pinned and must stay in place; others are normal data and can be treated as ordinary mutable references. This is exactly what pin projection is about.
一旦整个结构体被 pin 住,字段访问就会变得微妙。有些字段在逻辑上也必须跟着一起固定,有些字段则只是普通数据,依然可以按普通可变引用来处理。pin projection 解决的就是这件事。
The pin-project crate is the practical answer for most codebases because it generates the projection boilerplate correctly and safely.
对大多数代码库来说,pin-project 基本就是最实用的答案,因为它能把这些投影样板代码安全地自动生成出来。
Drop Ordering and ManuallyDrop
析构顺序与 ManuallyDrop
Rust’s drop order is deterministic:
locals drop in reverse declaration order, while struct fields drop in declaration order.
Rust 的析构顺序是确定的:局部变量按声明的逆序释放,结构体字段按声明顺序释放。
ManuallyDrop<T> suppresses automatic destruction so that low-level code can decide the exact moment when cleanup runs.ManuallyDrop<T> 则是用来阻止自动析构,让底层代码自己决定资源到底在什么时候清理。
#![allow(unused)]
fn main() {
use std::mem::ManuallyDrop;
struct TwoPhaseBuffer {
data: ManuallyDrop<Vec<u8>>,
committed: bool,
}
}
This is rarely needed in ordinary application code, but it becomes important in unions, unsafe abstractions, and custom lifecycle management.
这玩意儿在普通业务代码里很少需要,但在 union、unsafe 抽象和需要手工控制生命周期的底层代码里就会变得很重要。
Key Takeaways — Smart Pointers
本章要点 — 智能指针
Boxhandles single-owner heap allocation;RcandArchandle shared ownership in single-threaded and multi-threaded settings.Box负责单一所有者的堆分配;Rc和Arc分别负责单线程和多线程下的共享所有权。Weakis how reference-counted graphs avoid memory leaks from cycles.Weak是引用计数图结构避免环形泄漏的关键工具。CellandRefCellprovide interior mutability, butRefCellmoves borrow checking to runtime.Cell和RefCell提供内部可变性,而RefCell是把借用检查挪到了运行时。Cowhelps avoid unnecessary allocation,Pinhelps avoid invalid movement, andManuallyDrophelps control destruction precisely.Cow用来避免不必要分配,Pin用来避免非法移动,ManuallyDrop用来精确控制析构时机。
See also: Ch 6 — Concurrency for
Arc + Mutexpatterns, and Ch 4 — PhantomData for the relationship between phantom data and ownership semantics.
延伸阅读: 想看Arc + Mutex的并发组合,可以看 第 6 章:并发;想看 phantom data 和所有权语义的关系,可以看 第 4 章:PhantomData。
graph TD
Box["Box<T><br/>Single owner, heap<br/>单一所有者,堆分配"] --> Heap["Heap allocation<br/>堆分配"]
Rc["Rc<T><br/>Shared, single-thread<br/>单线程共享"] --> Heap
Arc["Arc<T><br/>Shared, multi-thread<br/>多线程共享"] --> Heap
Rc --> Weak1["Weak<T><br/>Non-owning<br/>非拥有"]
Arc --> Weak2["Weak<T><br/>Non-owning<br/>非拥有"]
Cell["Cell<T><br/>Copy interior mut<br/>基于复制的内部可变性"] --> Stack["Stack / interior<br/>栈上 / 内部状态"]
RefCell["RefCell<T><br/>Runtime borrow check<br/>运行时借用检查"] --> Stack
Cow["Cow<T><br/>Clone on write<br/>写时克隆"] --> Stack
style Box fill:#d4efdf,stroke:#27ae60,color:#000
style Rc fill:#e8f4f8,stroke:#2980b9,color:#000
style Arc fill:#e8f4f8,stroke:#2980b9,color:#000
style Weak1 fill:#fef9e7,stroke:#f1c40f,color:#000
style Weak2 fill:#fef9e7,stroke:#f1c40f,color:#000
style Cell fill:#fdebd0,stroke:#e67e22,color:#000
style RefCell fill:#fdebd0,stroke:#e67e22,color:#000
style Cow fill:#fdebd0,stroke:#e67e22,color:#000
style Heap fill:#f5f5f5,stroke:#999,color:#000
style Stack fill:#f5f5f5,stroke:#999,color:#000
Exercise: Reference-Counted Graph ★★ (~30 min)
练习:引用计数图结构 ★★(约 30 分钟)
Build a directed graph using Rc<RefCell<Node>> where each node has a name and a list of children. Create a cycle using Weak to break the back-edge, and verify with Rc::strong_count that the graph does not leak.
使用 Rc<RefCell<Node>> 构造一个有向图。每个节点都有名字和子节点列表。用 Weak 构造回边并打破引用环,再通过 Rc::strong_count 验证没有泄漏。
🔑 Solution
🔑 参考答案
#![allow(unused)]
fn main() {
use std::cell::RefCell;
use std::rc::{Rc, Weak};
struct Node {
name: String,
children: Vec<Rc<RefCell<Node>>>,
back_ref: Option<Weak<RefCell<Node>>>,
}
impl Node {
fn new(name: &str) -> Rc<RefCell<Self>> {
Rc::new(RefCell::new(Node {
name: name.to_string(),
children: Vec::new(),
back_ref: None,
}))
}
}
}