一句话总结
把 Rc(多所有者)和 RefCell(内部可变)组合成 Rc<RefCell<T>>, 就得到"既可共享, 又可修改"的数据--这是构建图, 树等结构的常用模式.
但它有个陷阱: 两个对象互相用 Rc 指向对方会形成循环引用, 导致引用计数永不归零, 内存泄漏.
破解之道是用 Weak 弱引用打破环.
经典组合: Rc<RefCell>
前两篇各留了一半: Rc 给多个所有者但只读, RefCell 给内部可变但单一所有者. 把它们嵌套起来, 两个能力就都有了:
|
读法是从外到内: Rc 负责"多个所有者共享", RefCell 负责"共享的同时还能改". 这个组合是单线程下构建可变共享数据结构(链表, 树, 图)的标准搭配.
Rc<RefCell<T>> 像一份多人共管, 可编辑的共享文档: Rc 是"多人都持有访问权"(共享所有权), RefCell 是"任何持有者都能编辑, 但同一时刻的编辑权要排他"(内部可变 + 运行期检查).
它在单线程里扮演的角色, 正是多线程里 Arc<Mutex<T>> 的角色(第 10 篇)--结构完全对应.
陷阱: 循环引用导致内存泄漏
引用计数有一个众所周知的软肋: 循环引用. 如果对象 A 用 Rc 指向 B, B 又用 Rc 指向 A, 它们的引用计数会互相托底, 永远不会归零:
|
|
Rust 也会内存泄漏?
是的. Rust 的安全保证针对的是"内存不安全"(悬垂, 越界, 数据竞争), 而内存泄漏(该释放的没释放)在 Rust 看来是"安全但不理想"--它不会导致崩溃或未定义行为, 因此不被编译器禁止.
Rc 循环引用是制造泄漏的主要途径. 这提醒我们: 安全 ≠ 没有任何资源问题, 设计共享结构时要警惕成环.
Weak: 不增加计数的"弱引用"
解法是 Weak<T>(弱引用). 它和 Rc 指向同一数据, 但有本质区别: Weak 不增加强引用计数, 因此不拥有数据, 不阻止其被释放.
Rc<T>(强引用) |
Weak<T>(弱引用) |
|
|---|---|---|
| 影响释放 | 计数不为零就不释放 | 不影响释放 |
| 表达的关系 | "我拥有它" | "我引用它, 但不拥有" |
| 访问数据 | 直接用 | 需 upgrade() 尝试升级为 Rc |
因为 Weak 指向的数据可能已被释放, 访问前必须用 upgrade() 检查--它返回 Option<Rc<T>>: 数据还在则 Some, 已释放则 None:
|
典型应用: 父子关系破环
最常见的破环场景是父子双向引用: 子节点需要访问父节点, 父节点也持有子节点. 规则是--所有权方向用 Rc, 反向用 Weak:
|
设计原则: 谁拥有谁, 决定强弱
判断该用 Rc 还是 Weak, 问一句: "这个引用是否表达所有权?"
父节点拥有子节点(子的存活由父负责), 所以父→子用 Rc; 子不拥有父(只是想能访问到), 所以子→父用 Weak. 这样环被打破: 即使外部释放, 父→子的强引用断开后, 子的计数能归零.
"所有权方向 Rc, 引用方向 Weak"是破环的通用心法.
快速回顾
- Rc<RefCell
> : 多所有者 + 内部可变, 单线程构建可变共享结构的标准组合; 对应多线程的Arc<Mutex<T>>. - 循环引用: 两个对象用 Rc 互指, 计数互相托底, 永不归零, 造成内存泄漏.
- Rust 允许泄漏: 泄漏是"安全但不理想", 不被编译器禁止; Rc 成环是主因.
- Weak: 弱引用, 不增加强计数, 不阻止释放; 访问需
upgrade()返回Option. - 破环心法: 表达所有权的方向用
Rc, 非所有权的反向引用用Weak(如父子关系).
动手练习
- 用一句话解释: 为什么
Rc<RefCell<T>>能同时做到"共享"和"可变", 而单独的 Rc 或 RefCell 不行?
参考答案
Rc 提供多所有者共享但只读; RefCell 提供内部可变但只能单一所有者. 嵌套后, Rc 负责"多个所有者共享同一份", RefCell 负责"被共享的数据仍可通过 borrow_mut 修改", 两者能力互补, 合起来即"可共享 + 可变".
-
下面构造了一个环. 函数结束时, a 和 b 的数据会被释放吗? 为什么?
let a = Rc::new(RefCell::new(Node::new()));
let b = Rc::new(RefCell::new(Node::new()));
a.borrow_mut().link = Some(Rc::clone(&b));
b.borrow_mut().link = Some(Rc::clone(&a));
参考答案
不会被释放, 发生内存泄漏. 函数结束时局部变量 a, b 离开作用域, 各自的强计数减一; 但由于 a 内部持有指向 b 的 Rc, b 内部持有指向 a 的 Rc, 两者的强计数都停在 1, 永远归不了零, 数据无法释放.
这是经典的 Rc 循环引用泄漏. 修法: 把其中一个方向的 Rc 改为 Weak.
Weak指向的数据可能已被释放, 所以访问要用upgrade(). 它返回什么类型? 如何使用?
参考答案
返回 Option<Rc<T>>: 数据还活着则得到 Some(Rc<T>)(同时强计数临时加一, 保证使用时它不被释放), 数据已释放则得到 None. 典型用法:
|
- 设计一棵树, 节点既要能访问子节点, 也要能访问父节点. 父子各用 Rc 还是 Weak? 给出理由.
参考答案
父→子用 Rc(强引用), 子→父用 Weak(弱引用). 理由: 父节点拥有子节点--子节点的存活应由父负责, 所以用表达所有权的 Rc; 子节点只是需要访问父节点而非拥有它, 用不增加计数的 Weak.
这样不会成环: 释放根节点后, 父→子的强引用逐层断开, 各节点计数能正常归零. 结构示意:
|