一句话总结
Rc(Reference Counted)突破了所有权"唯一所有者"的限制, 允许多个变量共同拥有同一份数据.
它内部维护一个引用计数: 每 clone 一次计数加一, 每个 Rc 销毁时计数减一, 归零时才释放数据.
代价是: 它只能用于单线程, 且共享的数据默认不可变.
所有权的"唯一所有者"撞墙了
到目前为止, 所有权规则要求每份数据有且只有一个所有者. 这在大多数时候很好, 但有些数据结构天然需要"被多个地方共同拥有". 最经典的例子是图或共享列表:
|
用 Box 做不到--因为 Box 是唯一所有权, a 一旦被移进 b, 就不能再移进 c:
|
这里"谁该是 a 的唯一所有者"根本没有答案--b 和 c 都需要它. 所有权模型在这类共享结构面前, 需要一个"放宽限制"的工具.
单一所有权像"一件东西只能登记在一个人名下". 可现实中有些东西本就该共有--像一份共享文档的所有权.
Rc 就是 Rust 的"共有产权登记": 多个主人共同持有, 直到最后一个主人离开, 东西才被处置.
Rc: 引用计数的共享所有权
Rc<T> 让多个所有者共享数据. "克隆"一个 Rc 不复制底层数据, 只是增加引用计数并得到一个新的指向同一数据的指针:
|
数据在引用计数归零时才被释放. 只要还有任何一个 Rc 活着, 数据就不会被清理--这正好满足"多个所有者"的需求.
Rc::clone 不是深拷贝, 别怕它贵
Rc::clone(&a) 只是把引用计数加一, 复制一个指针, 极廉价, 绝不会复制底层数据.
社区惯例特意写成 Rc::clone(&a) 而非 a.clone(), 就是为了在视觉上和"深拷贝的 clone"区分开--看到 Rc::clone 就知道"这只是加个计数".
关键限制: 共享的数据不可变
这是 Rc 最重要的约束, 也直接呼应借用规则. 既然多个 Rc 同时持有数据(等于多个共享引用同时存在), 根据"共享不可变"--Rc 只给不可变访问:
|
为什么必须不可变
设想若允许通过 Rc 修改: a 和 b 指向同一数据, 通过 a 改它, b 那边就会看到"数据在脚下变了"--这正是借用规则要杜绝的"共享 + 可变". 所以 Rc 坚持只读.
但"既想共享又想改"是真实需求--下一篇的 RefCell 就是来解决这个矛盾的. 记住这个伏笔: Rc 给共享, RefCell 给可变, 二者常组合.
第二个限制: 仅限单线程
Rc 的引用计数增减不是原子操作, 多线程同时改计数会产生数据竞争. 因此 Rc 被设计为不能跨线程(它没实现 Send/Sync, 第 09 篇详谈). 多线程下的共享所有权要用它的线程安全版本 Arc(Atomic Rc, 第 10 篇):
Rc<T> |
Arc<T> |
|
|---|---|---|
| 共享所有权 | 是 | 是 |
| 计数操作 | 普通(快) | 原子(略慢但线程安全) |
| 可跨线程 | 否 | 是 |
设计思想: 不为不用的安全付费
为什么不让 Rc 直接用原子计数, 一劳永逸? 因为原子操作有性能开销.
Rust 的哲学是"不为不需要的东西付费": 单线程场景占多数, 就给它零开销的 Rc; 需要跨线程时, 显式选 Arc 并接受那点原子开销. 把选择权和成本都摆在明面上--这正是 Rust 把 Go 隐藏在运行时里的取舍暴露给程序员的又一例.
快速回顾
- 解决的问题: 突破"唯一所有者"限制, 让多个变量共同拥有同一份数据(如共享列表, 图).
- 机制: 引用计数,
Rc::clone加一, 销毁减一, 归零才释放; clone 极廉价, 非深拷贝. - 限制一: 共享数据默认不可变(遵守"共享不可变"); 要可变需配 RefCell(下一篇).
- 限制二: 计数非原子, 仅限单线程; 跨线程用
Arc. - 设计思想: Rc/Arc 分立体现"不为不用的安全付费"--单线程零开销, 跨线程才付原子成本.
动手练习
-
下面代码为什么编译不过? 用 Rc 修复, 使 b, c 能共享 a.
let a = Box::new(5);
let b = vec![a];
let c = vec![a];
参考答案
Box 是唯一所有权, a 被移进 b 后就不能再移进 c. 改用 Rc 共享:
|
-
预测这段代码各处的引用计数.
use std::rc::Rc;
let a = Rc::new(1); // (A)
let b = Rc::clone(&a); // (B)
{
let c = Rc::clone(&a); // (C)
println!("{}", Rc::strong_count(&a)); // 此处?
} // (D)
println!("{}", Rc::strong_count(&a)); // 此处?
参考答案
第一个打印 3(a, b, c 三个 Rc 都活着). 内层作用域结束, c 被 drop, 计数减一. 第二个打印 2(只剩 a, b).
引用计数随 Rc 的创建与销毁动态增减, 归零时数据才释放.
-
为什么下面会报错? 这反映了 Rc 的什么限制?
use std::rc::Rc;
let a = Rc::new(String::from("hi"));
a.push_str("!");
参考答案
报错: 不能通过 Rc 获得可变访问. Rc 允许多个所有者共享, 等于多个共享引用并存, 遵守"共享不可变", 所以只给只读访问, push_str(需要 &mut)被拒.
要既共享又可变, 需要把数据包成 Rc<RefCell<T>>(下一篇).
- 什么时候该用
Rc, 什么时候该用Arc? 为什么不统一用Arc?
参考答案
单线程内共享用 Rc, 需要跨线程共享用 Arc. 不统一用 Arc, 是因为 Arc 的引用计数是原子操作, 有性能开销; 单线程场景不需要这种线程安全, 用零开销的 Rc 更合适.
这体现 Rust "不为不用的东西付费"的设计哲学--把成本和选择权交给程序员.