智能指针:让资源归属可组合
课程概览 · 第 15 章
当任务列表需要被多个视图同时看到、当任务树需要递归引用自身时,普通所有权开始显得不够用。Rust 的答案不是放宽规则,而是给指针加上明确语义:智能指针回答"数据归谁、谁能改、借用检查发生在什么时候"。本章把 Box/Rc/Arc/RefCell/Cell 放回任务队列/CLI 这个熟悉的业务域,讲清什么时候该用、什么时候不该升级。
学习目标与默认选择
学完本章你应当能够:
- 按所有权(唯一/共享)、线程可用性(单线程/跨线程)、运行期借用行为(有无运行期检查)三个维度为
Box<T>、Rc<T>、Arc<T>、RefCell<T>、Cell<T>做选型; - 判断"多个所有者"是真实需求还是设计偷懒,并说出
Rc<RefCell<T>>这类升级组合的代价。
默认选择:先普通所有权 + 引用;递归类型用 Box;只有当"多个所有者"是真实需求(不是设计偷懒)时才用 Rc/Arc;RefCell/Cell 是把借用检查移到运行期的升级选择。
概念讲解:智能指针是"带语义的所有权"
指针选择表
先给结论表,随后逐个用可运行示例验证:
| 类型 | 所有权 | 线程可用性 | 运行期借用行为 | 典型场景 |
|---|---|---|---|---|
Box<T> | 唯一所有者,值在堆上 | Send(若 T: Send) | 无(编译期检查) | 递归类型、大值转移、trait 对象 |
Rc<T> | 共享所有(引用计数) | 仅单线程(!Send 且 !Sync) | 无(共享不可变) | 单线程多视图共享 |
Arc<T> | 共享所有(原子引用计数) | 跨线程 | 无(共享不可变) | 多线程共享只读配置 |
RefCell<T> | 唯一,但允许运行期可变借用 | 仅单线程 | 运行期检查,违规 panic | 与 Rc 组合实现单线程共享可变 |
Cell<T> | 唯一 | 仅单线程 | 无借用,仅 Copy 值的取/放 | 计数器、标志位 |
读表的要点:Rc/Arc 只解决所有权共享,不提供修改能力;RefCell/Cell 只解决运行期可变性,不解决共享。两者正交,所以才有 Rc<RefCell<T>> 这种组合——但它是两个升级叠加,应当显式承认。
Box<T>:递归类型的唯一所有者
任务树的节点要么是叶子,要么包含子节点。如果 TaskNode 直接包含 Vec<TaskNode>,类型大小可以确定(Vec 本身是胖指针),不需要 Box;但当子节点是单数时,就必须用 Box 打破"大小包含自身"的无限递归:
1 | |
Box 的全部语义就这些:堆分配、唯一所有者、离开作用域自动释放。它不是 Rc 的廉价版,也不能共享。
Rc<RefCell<T>>:单线程共享可变(升级选择)
场景:一个任务列表同时被"统计视图"和"编辑器"持有,谁都不拥有谁。用 Rc 共享 Vec<Task>,用 RefCell 允许其中一个句柄修改内容:
1 | |
注意这被明确标注为升级选择:这个例子完全可以用"一个拥有者 + 传递 &mut"改写。Rc<RefCell<T>> 适用的时刻是:多个句柄的生命周期互相独立、无法找到一个自然的唯一拥有者。即便如此也要付出代价——借用冲突从编译错误变成运行期 panic:
1 | |
(上例中 Task 等类型定义与前一示例相同,请在同一程序内复制使用;各示例间不共享定义。)
Cell<T>:Copy 值的内部可变性
Cell<T> 不提供借用,只提供 get/set,因此只适合小型 Copy 值。典型用途是在不可变结构中藏一个计数器:
1 | |
Cell 避开了"运行期借用记账",代价是值必须 Copy——你不能从 Cell<String> 拿出 &String。
Arc<Mutex<T>> 也是升级选择
多线程版本同样成立:Arc<Mutex<T>> = 共享所有权 + 互斥修改,两个升级叠加。它不是"清晰所有权的替代品",而是当所有权无法收敛到单一线程时的兜底方案(第 16 章展开)。
边界与失败场景
Rc跨线程:Rc<T>是!Send,把它 move 进thread::spawn的闭包直接编译失败(第 16 章演示错误文本)。跨线程共享用Arc。RefCell双重借用:同一作用域内两次borrow_mut()在运行期 panic(见上文 text 示例)。这是把编译期保证换成运行期检查的代价,测试必须覆盖借用重叠路径。Cell放非Copy值:Cell<String>无法get()(String不是Copy),只能take/replace,此时通常应改用RefCell。- 引用环:
Rc相互引用形成环后引用计数永不清零,内存泄漏。需要环(如图结构)时用Weak打破。
为什么可行:借用检查只换了位置,没有消失
普通引用的"多读或一写"由编译器在编译期证明;RefCell 把同样的规则搬进运行期——borrow()/borrow_mut() 仍是那套规则,只是违规从编译错误变成 panic。Rc/Arc 并没有引入"任意共享修改":它们共享的是不可变访问,修改仍要再叠一层 RefCell/Mutex。换句话说,所有智能指针都只是把同一个所有权模型在不同维度上重新分配检查位置;不存在绕过该模型的指针。
常见误区
Box当共享用:Box是唯一所有者,克隆它的唯一途径是克隆内容。Arc被当成"线程安全的可变全局":Arc<T>只共享T的只读视图;修改要Arc<Mutex<T>>,且临界区要短(第 16 章)。Rc<RefCell<T>>满天飞:这是设计妥协的信号。先问"能否让一个组件拥有数据、其他组件持有引用",多数 GUI 式共享可以通过重排所有权消除。
自测
Box<TaskNode>、Rc<RefCell<Vec<Task>>>、Arc<Mutex<Vec<Task>>>各自的"所有权/线程/运行期借用"三维坐标是什么?
答的方向:唯一/单线程/编译期;共享/单线程/运行期检查;共享/多线程/互斥(阻塞等待而非 panic)。- 为什么
Cell<String>不能get()?什么类型可以?
答的方向:get返回T需要T: Copy,否则会把值移出且原值失效;只有Copy类型适用Cell的取/放模型。 - 什么时候
Rc<RefCell<T>>是正确选择,什么时候它是设计气味?
答的方向:多个生命周期独立的句柄确实需要共享可变数据时正确;当能找到自然唯一拥有者(改为传递&mut或返回新值)时它是气味。






