要点
Box 提供堆上的唯一所有权,Rc/Arc 共享所有权,RefCell/Mutex 提供不同的内部可变机制。共享所有权不自动使内容可安全修改。
目录
核心概念
概念一:智能指针是”带行为的所有者”
定义: 智能指针是实现了 Deref(自动解引用)与 Drop(自动清理)的结构体,在拥有数据的同时附加额外元数据与行为——引用计数就是最典型的附加元数据。
关键特性: – Box<T>:唯一所有权 + 堆分配,最简单也最常用 – Rc<T>(单线程)/ Arc<T>(多线程):共享所有权,clone 只加计数不复制数据 – RefCell<T>:把”同一时刻最多一个可变借用”的检查从编译期挪到运行期(内部可变性) – Mutex<T>:RefCell 的线程安全版,加锁才能访问
使用场景: – 递归类型、大对象转移、trait 对象(Box<dyn Trait>)→ Box – 多个线程累积统计、共享配置 → Arc<Mutex >
概念二:组合拳的三种典型形态
定义: 智能指针很少单独登场,按”共享 × 可变”两轴组合。
关键特性: – Rc<T>:共享 + 只读(单线程) – Rc<RefCell<T>>:共享 + 内部可变(单线程),编译期让位、运行期付检查成本 – Arc<Mutex<T>>:共享 + 互斥可变(多线程),运行期付锁成本
使用场景: – 线程池 worker 共享任务队列 → Arc<Mutex<VecDeque<T>>> – 多线程只读配置 → Arc<T>(无需 Mutex)
概念三:Deref 强制转换与 Drop 语义
定义: Deref 让 &Box<String> 自动一路转成 &str(编译器的 deref coercion);Drop 让资源在离开作用域时确定性释放——这是 Rust 无 GC 却能自动管理内存的关键。
关键特性: – Deref 是方法调用与传参的隐形转换链:MyBox<String> → String → str – Drop 按变量声明的逆序执行(栈式清理),std::mem::drop 可提前释放 – MutexGuard 正是靠 Drop 实现解锁——离开作用域即释放锁,忘记解锁是编译不过的代码设计
使用场景: – 包装类型希望”用起来像内部类型” → 实现 Deref – 文件句柄、锁、网络连接等需要确定性清理的资源 → 实现 Drop
实践指南
步骤一:用 Box 打通递归类型
目标: 理解”编译期确定大小”约束与间接化方案。
操作指南: 1. 尝试定义 enum List { Cons(i32, List), Nil },观察递归大小报错 2. 把递归分支改为 Box<List>,编译通过 3. 用 println!("{list:?}") 验证 Debug 输出
验证方法: 编译无错;把 Box 去掉复现 recursive without indirection 报错。
步骤二:观察 Rc 的计数变化
目标: 建立引用计数的直觉。
操作指南: 1. Rc::strong_count(&a) 在 clone 前后打印计数 2. 在内层作用域再 clone 一次,观察计数上升与回落 3. 尝试对 Rc<T> 里的数据调用可变方法,确认编译拒绝
验证方法: 计数输出与预期一致(1 → 2 → 3 → 2)。
步骤三:体验 RefCell 的运行时检查
目标: 分清编译期借用检查与运行期借用检查的边界。
操作指南: 1. 正常流程:borrow_mut() 修改、borrow() 读取 2. 取消示例三中 b2 注释行,运行观察 panic(而不是编译报错) 3. 用 drop(b1) 显式释放后再借用,恢复正常
检查方法: 在项目中运行示例六的 should_panic 测试,并核对它确实因预期原因 panic。
步骤四:升级到 Arc<Mutex>
目标: 把单线程共享模式平移到多线程。
操作指南: 1. Rc 换成 Arc,RefCell 换成 Mutex,其余结构不变 2. 注意 Arc::clone(&counter) 必须每次循环都做(move 闭包带走自己的计数) 3. lock() 返回 Result,unwrap 是最直接的毒化策略;MutexGuard 离开作用域自动解锁
检查方法: 让 10 个线程各加 1,并核对本次运行的结果为 10;这只能检查该示例,不能取代并发设计审查。
代码示例
示例一:Box 与递归类型
// 块1:Box 与递归类型
fn main() {
// Box:堆分配的唯一所有权智能指针
let b = Box::new(5);
println!("b = {b}"); // Deref 自动解引用,像用 i32 一样用 Box<i32>
// 递归类型:编译期无法确定大小,必须经过指针间接化
#[derive(Debug)]
#[allow(dead_code)] // 教学示例:字段仅用于展示结构,不参与读取
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
let list = Cons(1, Box::new(Cons(2, Box::new(Nil))));
println!("{list:?}");
}
关键点解析: – Box<List> 大小固定(一个指针),递归得以终止计算 – Box 也常用于 Box<dyn Trait>(trait 对象,动态分发)与搬运大结构体避免栈拷贝
示例二:Rc 引用计数
// 块2:Rc 引用计数
use std::rc::Rc;
fn main() {
// Rc:单线程引用计数,多个所有者共享只读数据
let a = Rc::new(String::from("共享配置"));
println!("初始计数 = {}", Rc::strong_count(&a));
let b = Rc::clone(&a); // 只增加计数,不复制数据
println!("clone 后 = {}", Rc::strong_count(&a));
{
let c = Rc::clone(&a);
println!("内层 = {}", Rc::strong_count(&a));
println!("b 与 c 看到同一份数据:{b} / {c}");
} // c 离开作用域,计数回落
println!("离开作用域后 = {}", Rc::strong_count(&a));
}
关键点解析: – Rc::clone(&a) 与 a.clone() 等价,但前一种写法明确表达”这是引用计数操作,不是数据复制” – 计数归零时数据才释放——多个所有者共享同一次堆分配
示例三:Rc<RefCell<T>> 内部可变性
// 块3:Rc<RefCell<T>> 内部可变性
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
// Rc<RefCell<T>>:多所有者共享 + 单线程内部可变
let shared = Rc::new(RefCell::new(vec![1, 2, 3]));
let writer = Rc::clone(&shared);
writer.borrow_mut().push(4); // 可变借用
let reader = Rc::clone(&shared);
println!("reader 看到 {:?}", reader.borrow()); // 不可变借用
// 运行时规则:同一时刻不能有两个可变借用
let b1 = shared.borrow_mut();
// let b2 = shared.borrow_mut(); // 取消注释即 panic:already borrowed
drop(b1); // 显式释放,恢复可借用状态
println!("drop 后可再次借用: {:?}", shared.borrow());
}
关键点解析: – 编译期规则”多个 & 或一个 &mut“在 RefCell 场景下变成运行期检查:违规即 panic,错误推迟到运行时 – drop(b1) 提前释放借用是避免 panic 的常用手段;更稳的写法是缩小借用所在的作用域
示例四:Arc<Mutex<T>> 多线程共享
// 块4:Arc<Mutex<T>> 多线程共享
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
// Arc<Mutex<T>>:跨线程共享 + 互斥可变
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap(); // 锁毒化时 unwrap 快速失败
*num += 1;
}); // MutexGuard 在此自动释放(RAII),无需手动 unlock
handles.push(handle);
}
for h in handles {
h.join().unwrap();
}
println!("结果 = {}", *counter.lock().unwrap()); // 恒为 10,无数据竞争
}
关键点解析: – Rc 不是 Send,跨线程编译直接报错——Arc 的原子计数就是为多线程准备的 – 没有 Mutex 的 Arc<i32> 无法在子线程里 +=:共享可变性必须有锁(或原子类型)
示例五:自定义智能指针(Deref 与 Drop)
// 块5:Deref 与 Drop
use std::ops::Deref;
// 自定义智能指针:实现 Deref 获得自动解引用与方法穿透
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> Self {
MyBox(x)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
fn greet(name: &str) {
println!("你好,{name}");
}
// Drop:离开作用域时自动执行清理
struct Resource(String);
impl Drop for Resource {
fn drop(&mut self) {
println!("释放资源:{}", self.0);
}
}
fn main() {
let m = MyBox::new(String::from("Rust"));
// Deref 强制转换链:&MyBox<String> -> &String -> &str
greet(&m);
let _r1 = Resource(String::from("文件句柄"));
let _r2 = Resource(String::from("网络连接"));
println!("main 即将结束,Drop 按声明逆序执行");
}
关键点解析: – greet(&m) 无需任何显式转换:编译器沿 Deref 链自动走到 &str – Drop 逆序输出(”网络连接”先于”文件句柄”)——栈式清理保证资源依赖正确
示例六:共享句柄 + 单元测试
// 块6:共享句柄 + 单元测试
use std::cell::RefCell;
use std::rc::Rc;
fn push_shared(shared: &Rc<RefCell<Vec<i32>>>, v: i32) {
shared.borrow_mut().push(v);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn 两个句柄看到同一份数据() {
let shared = Rc::new(RefCell::new(vec![]));
push_shared(&shared, 1);
push_shared(&shared, 2);
assert_eq!(shared.borrow().len(), 2);
}
#[test]
#[should_panic]
fn 双重可变借用运行时_panic() {
let shared = Rc::new(RefCell::new(0));
let _b1 = shared.borrow_mut();
let _b2 = shared.borrow_mut(); // 运行时 panic,而非编译错误
}
}
测试的组织方式详见 10-Cargo 工程化与单元测试。
最佳实践
Box 表达独占堆分配,Rc/Arc 表达共享所有权,RefCell/Mutex 等解决不同环境中的内部可变性,不能简单当作逐层升级的工具。Rc::clone 复制持有句柄,需与数据类型自己的 Clone 行为区分。
RefCell 冲突在运行时检查,问题是同时存在不兼容借用,而不是遇到问号操作符就危险。缩短借用与锁 guard 的作用域,避免回调重入或长 I/O。共享引用形成环时用 Weak 等设计打破所有权环,并验证释放行为。
常见问题
Q1: Box、Rc、Arc 的 clone 有什么区别?
A: Box<T> 在 T: Clone 时通过 T::clone 复制内部值;是否深度复制取决于 T 的 Clone 契约;Rc/Arc 的 clone 只增加引用计数,数据不复制。这正是 Rc::clone(&a) 写法被推荐的原因——语义自解释。
Q2: RefCell 和 Mutex 是什么关系?
A: 它们都允许在共享引用下改变值,但解决的约束不同。RefCell<T> 用运行时借用标志执行单线程的借用规则:冲突借用会 panic;Mutex<T> 用互斥保护跨线程访问:竞争时会等待,错误的锁顺序、重入或 guard 生命周期可能造成死锁。Mutex 的实现是否进入内核取决于竞争与平台,不能据此推断固定性能。Rc<RefCell<T>> 与 Arc<Mutex<T>> 的类型形状相似,却不能机械替换:迁移后要重新审查 Send/Sync、锁粒度、等待期间是否持锁,以及错误与取消路径。
Q3: 什么时候必须用 Box 而不是泛型?
A: 需要”同一集合里放多种实现”或”编译期未知的具体类型”时。泛型是编译期单态化(每种类型一份代码),trait 对象是运行期动态分发(虚表)。深入对比见 trait 对象与动态分发(字典篇)。
相关资源
延伸阅读
- 官方文档: The Rustbook ch.15 – Smart Pointers – 智能指针官方章节
- 官方文档: std::cell 与 std::sync – 内部可变性与线程同步全量 API
工具资源
- 开发工具: Rust Playground – 复现 Rc 循环引用与 RefCell panic
练习与实践
练习一:共享计数器双版本
目标: 同一逻辑的单线程/多线程实现。
任务要求: 1. 用 Rc<RefCell<i32>> 实现单线程计数器函数,任意次调用累计 2. 用 Arc<Mutex<i32>> + 8 线程实现并行版本,结果可预测 3. 为两个版本各写 2 个单元测试
评估标准: 并行版结果恒正确;单线程版若尝试跨线程使用,编译器能给出 Send 相关报错。
知识图谱
前置知识
graph TD
A[所有权与借用] --> B[生命周期标注]
B --> C[智能指针]
后续学习
graph LR
C[智能指针] --> D[Weak 与循环引用]
C --> E[Arc + Tokio 异步共享状态]
文档交叉引用
相关文档
- 📄 所有权与借用: RefCell 是编译期规则的运行期版本
- 📄 生命周期标注: 生命周期写不动时的替代方案
- 📄 集合与迭代器: MutexGuard 借助迭代器思想理解 RAII
- 📄 Cargo 工程化与单元测试: 示例六测试的组织方式
参考章节
- 📖 trait 对象与动态分发(字典):
Box<dyn Trait>的机制细节 - 📖 所有权字典: 借用检查细则
- 📖 模块 README: 技术基线与模块路径图
总结
核心要点回顾
- 按”共享 × 可变”两轴选型: Box→Rc→Rc→Arc,能力递增、成本递增
- RefCell 把检查挪到运行时: 换来更灵活的共享可变,代价是 panic 风险
- Deref/Drop 是智能指针的行为契约: 自动解引用让包装透明,Drop 让清理确定且逆序
学习成果检查
- [ ] 能说出 Box/Rc/Arc 各自解决的问题?
- [ ] 能解释 RefCell 违规借用何时 panic、如何用 drop 避免?
- [ ] 能把 Rc<RefCell> 版本代码机械改写为 Arc<Mutex>?
- [ ] 能实现一个自定义 Deref/Drop 类型并预测其输出顺序?
贡献与反馈
发现本文档有改进空间?欢迎在 Issues 报告问题、提出改进建议,或直接提交 PR 完善内容。
文档版本: v1.0.0最后更新: 2026年9月维护团队: Hello Team
💡 学习建议: 智能指针是”为所有权规则付费”的方式——每次用到先自问:这个复杂度能不能用更简单的所有权结构避免?
🎯 下一步: Arc 正是多线程共享状态的核心工具。继续学习 09-并发与 async。