智能指针:让资源归属可组合
课程概览 · 第 17 章
当任务链需要递归包含自身、任务列表需要被多个视图持有时,关键不是找一个“更方便的引用”,而是决定数据归谁、谁能修改、检查发生在何时。本章的示例彼此独立,只声明当前操作需要的字段,不复刻完整任务领域模型。
前置知识与学习目标
需要第 06 章的所有权、借用与作用域,以及第 10 章的 trait 约束。学完本章应能:
- 按独占、共享、修改能力与线程条件选择指针,而不是遇到借用错误就升级。
- 用
Box构造有限大小的递归类型,用Rc保持多个独立句柄。 - 预测
RefCell的运行期借用冲突,区分Cell各方法的约束。 - 解释
Deref、方法查找与as_deref的边界。
先按能力选型
默认让一个组件拥有数据,其他组件借用 &T 或 &mut T。只有需要独立存活的共享句柄时才使用引用计数;需要通过共享引用修改内部数据时,再选择内部可变性。
| 类型 | 归属与修改能力 | 线程条件 | 检查方式 |
|---|---|---|---|
Box<T> | 堆上独占所有权,普通借用可修改 | T: Send 时 Send,T: Sync 时 Sync | 编译期借用检查 |
Rc<T> | 单线程共享所有权 | 不实现 Send/Sync | 非原子引用计数;修改能力由 T 决定 |
Arc<T> | 原子计数的共享所有权 | 通常跨线程共享要求 T: Send + Sync | 不会自动让 T 的访问线程安全 |
RefCell<T> | 共享引用可申请动态借用 | T: Send 时 Send,但不 Sync | 多读或一写,冲突 panic |
Cell<T> | 共享引用可按值替换内部值 | T: Send 时 Send,但不 Sync | 不交出内部共享引用;get 要求 Copy |
Send 是值可以跨线程移动,Sync 是共享引用可以跨线程共享。Cell/RefCell 可以在满足条件时整体移动到另一线程,不等于可以由多个线程同时访问。参见 Arc 的线程安全条件。
Box:打破递归大小
直接包含 Option<TaskNode> 会使大小递归依赖自身;Option<Box<TaskNode>> 通过固定大小的间接层打破这个循环。若包含的是 Vec<TaskNode>,Vec 描述符的大小已经确定,不必为这个原因再套 Box;Vec 也不是只有数据指针和长度的“胖指针”。
独立示例(仅标准库;保存为 src/main.rs,执行 cargo +stable run):
1 | |
Box 是独占所有者,不是共享指针。销毁 Box 会释放其持有的值;克隆 Box<T>(要求 T 可克隆)会克隆内容,而非只增加一个共享句柄。
Rc 与 Arc:共享归属,不自动共享修改
Rc::clone 增加强引用计数,不复制内部数据。最后一个强引用销毁后,内部值才被销毁。跨线程时使用 Arc 的原子引用计数,但它仍不提供内部数据的同步:Arc<RefCell<T>> 不会因此变成线程安全的共享可变状态。
独立示例(仅标准库;src/main.rs,cargo +stable run):
1 | |
title.clone() 同样先匹配 Rc 自己的 clone 方法;(*title).clone() 则把接收者换成 String,复制字符串内容。若两个 Rc 相互持有强引用,计数可能永远不归零。图结构中的反向边通常使用 Weak:Rc::downgrade 不增加强计数,Weak::upgrade 返回 Option,必须处理对象已销毁的情况。
RefCell 与 Cell:通过共享引用修改
RefCell:借用规则在运行期检查
borrow() 返回共享借用 guard,borrow_mut() 返回独占借用 guard;guard 的销毁结束对应借用。规则仍是多读或一写,但违规从编译错误变成运行期 panic。输入驱动的借用冲突需要返回错误时,可以用 try_borrow/try_borrow_mut 而不是靠 panic 分支控制流程。
故意失败:运行期 panic(仅标准库;独立 src/main.rs。编译应成功,第二次 borrow_mut 冲突):
1 | |
独立示例(仅标准库;src/main.rs,cargo +stable run):
1 | |
Cell:按值换入换出
Cell::get 要求 T: Copy,适合计数器;Cell 本身并不限于 Copy 类型。set 替换并销毁旧值,replace 换出旧值,into_inner 消费整个 Cell 取出 T;take 用默认值替换,要求 T: Default。它不提供通过共享引用取得 &String 的接口。
独立示例(仅标准库;src/main.rs,cargo +stable run):
1 | |
能力组合:Rc<RefCell> 与 Arc<Mutex>
一个编辑器与一个统计视图确实需要独立持有任务列表时,可组合共享归属与动态修改能力。如果可以找到自然的唯一拥有者,传递 &mut 通常更简单。
独立示例(仅标准库;src/main.rs,cargo +stable run):
1 | |
跨线程共享修改通常组合 Arc<Mutex<T>>,并要求内部 T 满足对应的 Send 条件。锁保护访问,Arc 保护共享归属,两者不能互相替代。短临界区的标准库锁不要跨 await;确实需要等待期间持锁时,另行评估异步锁与死锁风险,第 18 章展开。
深入阅读:Deref 与自动解引用
引用强制转换与方法查找是两种机制
在函数实参、带类型标注的绑定等转换点,&Box<String> 可以通过 Deref 强制转换成 &str;转换不移动 String,也不复制其字节。本例的标准库 Deref 实现只是取得内部引用,自定义 Deref 仍可能执行代码,不能概括成任意 Deref 都没有计算成本。
独立示例(仅标准库;src/main.rs,cargo +stable run):
1 | |
&[T; N] 到 &[T] 是 unsize 强制转换,不是 Deref。引用强制转换也不会反过来自动分配 String。
故意失败:编译错误(仅标准库;独立 src/main.rs。预期类型不匹配,不是生命周期错误):
1 | |
方法调用会按规则构造自动解引用及借用候选,再查找适用方法;外层方法可以先匹配。它不是继承,也不承诺一个通用的固定自动解引用层数。遇到递归上限应检查类型设计,而非依赖某个编译器默认数字。
Option 不会自动穿透,trait bound 也不会转发
Option<Box<TaskNode>> 没有 Deref 到 TaskNode,因此不能直接访问 .title。as_deref 借用 Option 内的 Box,再把其目标引用映射为 Option<&TaskNode>;本章 Box 链已经演示这条路径。
故意失败:编译错误(仅标准库;独立 src/main.rs。Option 没有 title 字段):
1 | |
Deref 也不会让泛型 trait 约束自动成立。下面 Wrapper 可以调用 String 的 len 方法,但自身仍没有 Display 实现。newtype 不应仅为省略 .0 就暴露底层全部可见方法。
故意失败:编译错误(仅标准库;独立 src/main.rs。Wrapper 未实现 Display):
1 | |
Box/Rc 的 Display 等转发来自标准库对相应 trait 的实现,而不是 Deref 自动推导。
自测
- Box
、Rc 、Arc 的跨线程条件有什么不同? - 为什么 RefCell 的两次可变借用能编译,却可能运行期 panic?
- Cell
不能 get,是否意味着它不能存 String?如何换出旧值? - Rc<RefCell
> 分别增加了什么能力,何时应保留普通所有权? - 为什么 Option<Box
> 需要 as_deref,Wrapper 的 Deref 又不能满足 Display?
参考答案
- Box 分别随 T: Send/T: Sync 实现对应能力;Rc 不 Send/Sync;通常 Arc 跨线程共享要求 T: Send + Sync,原子计数不保证 T 本身线程安全。
- guard 重叠违反运行期多读或一写规则,第二次 borrow_mut 检查失败;显式缩短 guard 作用域可避免本例冲突。
- 可以存。get 要求 Copy;replace 返回旧 String,take 要求 Default,into_inner 消费 Cell。
- Rc 增加独立共享句柄,RefCell 增加共享引用下的动态借用。能找到自然唯一拥有者时优先传引用。
- Option 不 Deref 到内部目标,as_deref 显式在容器内部映射;方法查找可解引用,不等于外层类型实现了 trait。






