本篇导读
上一篇的 Box<T> 是独占所有权的智能指针:一份数据只有一个 Box 拥有。但真实程序中,我们常常需要:
- 一份数据被多个地方共同拥有(如图中被多个节点引用的节点、UI 中被多处共享的资源);
- 在整体不可变的外表下,修改其内部某些数据(内部可变性)。
今天我们学习三个紧密相关的智能指针:
一、Rc<T>:让一份数据有多个所有者
Rc 是 Reference Counting(引用计数)的缩写。每当克隆一个 Rc,计数加一;每当一个 Rc 离开作用域,计数减一;计数归零时数据被释放。它是单线程场景下共享数据的方案(多线程用 Arc,第 18 篇会讲到)。
1. 为什么需要它:共享的 cons list
假设我们希望两个链表共享同一段公共尾部:
enum List {
Cons(i32, Box<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
let b = Cons(3, Box::new(a)); // a 被移动进 b
// let c = Cons(4, Box::new(a)); // 错误:a 已经被移走
}
用 Rc 替换 Box:
use std::rc::Rc;
enum List {
Cons(i32, Rc<List>),
Nil,
}
use List::{Cons, Nil};
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
println!("创建 a 后计数:{}", Rc::strong_count(&a)); // 1
let b = Cons(3, Rc::clone(&a)); // 克隆 Rc,计数 +1(不复制底层数据)
println!("创建 b 后计数:{}", Rc::strong_count(&a)); // 2
{
let c = Cons(4, Rc::clone(&a));
println!("创建 c 后计数:{}", Rc::strong_count(&a)); // 3
} // c 离开作用域,计数回到 2
println!("内层结束后计数:{}", Rc::strong_count(&a)); // 2
}
2. Rc::clone 是廉价的
Rc::clone(&a) 只增加计数、复制一个指针,不会深拷贝底层数据,所以非常廉价。惯例用 Rc::clone 而非 a.clone(),便于在代码中识别"这是引用计数克隆"。
3. Rc 是不可变共享
通过 Rc 你只能获得不可变引用。这保证多所有者共享时不会有人修改数据。若需要可变,就要配合下面的 RefCell。
二、RefCell<T>:内部可变性
1. 内部可变性模式
正常情况下,借用规则在编译期强制:不可变就是不能改。内部可变性(interior mutability)是一种设计模式:一个类型对外提供不可变接口,但内部允许修改自身数据,把借用检查从编译期推迟到运行时。RefCell<T> 就是它的标准实现。
2. 什么时候用
- 你能确信运行时借用规则不会被违反,但编译器无法在编译期证明(它只做局部、保守的分析);
- 常见于:测试 mock 对象、需要可变缓存的不可变对象、某些设计模式。
3. 基本用法:borrow 与 borrow_mut
use std::cell::RefCell;
fn main() {
let cell = RefCell::new(5);
// borrow_mut 获得可变借用
*cell.borrow_mut() += 10;
*cell.borrow_mut() += 2;
// borrow 获得不可变借用
println!("值:{}", cell.borrow()); // 17
}
borrow() 返回 Ref<T>,borrow_mut() 返回 RefMut<T>;
- 它们在运行时执行借用规则:同一时刻多个 borrow,或一个 borrow_mut;
- 如果运行时违反(比如同时持有两个 borrow_mut),程序会 panic,而不是允许数据竞争。
4. Rc + RefCell:可变的多所有者
二者组合极其强大:Rc 提供多个所有者,RefCell 提供可变性。
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let shared = Rc::new(RefCell::new(vec![1, 2, 3]));
let a = Rc::clone(&shared);
let b = Rc::clone(&shared);
a.borrow_mut().push(4); // 通过 a 修改
b.borrow_mut().push(5); // 通过 b 修改
println!("{:?}", shared.borrow()); // [1, 2, 3, 4, 5]
}
5. RefCell 的代价
- 借用检查发生在运行时,违反会 panic(而非编译报错);
- 有极小的计数性能开销;
- 只能用于单线程(Rc、RefCell 都不实现 Send)。多线程对应
Arc<Mutex<T>>。
三、引用循环与内存泄漏
Rust 几乎不可能发生普通内存泄漏,但 Rc + RefCell 组合可能制造引用循环:A 指向 B、B 又指向 A,两者计数永不为零,数据永不释放。
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Option<Rc<Node>>>,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(None),
children: RefCell::new(vec![]),
});
let root = Rc::new(Node {
value: 5,
parent: RefCell::new(None),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
// 让 leaf 指回 root —— 如果用 Rc,就形成循环
*leaf.parent.borrow_mut() = Some(Rc::clone(&root));
// root -> leaf, leaf -> root:计数无法归零
}
四、Weak<T>:用弱引用打破循环
解决办法是把"回指"的一方改成弱引用 Weak<T>。Weak 不增加 strong_count,不拥有数据,也不阻止数据被释放;访问前要 upgrade 成 Option<Rc>。
把 Node 的 parent 字段改为 Weak:
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>, // 弱引用指回父节点
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
let root = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
// Rc::downgrade 创建弱引用
*leaf.parent.borrow_mut() = Rc::downgrade(&root);
// upgrade 访问:数据还在返回 Some,已释放返回 None
if let Some(parent) = leaf.parent.borrow().upgrade() {
println!("leaf 的父节点值:{}", parent.value); // 5
}
}
强弱引用的规则
- 强引用计数
strong_count 归零才释放数据;
- 弱引用计数
weak_count 不阻止释放;
- 树结构惯例:父节点用 Rc 强拥有子节点,子节点用 Weak 弱指回父节点,这样从 root 释放时整棵树都能正确回收。
五、Cell:Copy 类型的轻量内部可变性
对于实现了 Copy 的简单类型(如 i32),还可以用更轻的 Cell<T>,通过 get/set 操作、无需引用:
use std::cell::Cell;
let c = Cell::new(1);
c.set(10);
println!("{}", c.get()); // 10
c.update(|x| x + 5);
Cell 不提供引用、只整体替换值,因此比 RefCell 更轻、不会 panic,但只适合 Copy 类型。
六、综合示例:可双向遍历的树
use std::cell::RefCell;
use std::rc::{Rc, Weak};
type NodeRef = Rc<RefCell<TreeNode>>;
type WeakNode = Weak<RefCell<TreeNode>>;
struct TreeNode {
value: i32,
parent: Option<WeakNode>,
children: Vec<NodeRef>,
}
impl TreeNode {
fn new(value: i32) -> NodeRef {
Rc::new(RefCell::new(TreeNode {
value,
parent: None,
children: Vec::new(),
}))
}
fn add_child(parent: &NodeRef, child: &NodeRef) {
parent.borrow_mut().children.push(Rc::clone(child));
child.borrow_mut().parent = Some(Rc::downgrade(parent));
}
}
fn main() {
let root = TreeNode::new(1);
let child1 = TreeNode::new(2);
let child2 = TreeNode::new(3);
TreeNode::add_child(&root, &child1);
TreeNode::add_child(&root, &child2);
TreeNode::add_child(&child1, &TreeNode::new(4));
// 从 child1 向上找父节点
let parent_value = child1
.borrow()
.parent
.as_ref()
.and_then(|w| w.upgrade())
.map(|p| p.borrow().value);
println!("child1 的父节点值:{parent_value:?}"); // Some(1)
}
这是树/图结构中 Rc、Weak、RefCell 组合的完整范式。
七、本篇小结
Rc<T> 用引用计数实现单线程多所有者,Rc::clone 廉价、只增计数,只提供不可变共享;
RefCell<T> 实现内部可变性,borrow/borrow_mut 在运行时检查借用规则,违反则 panic,仅单线程;
Rc<RefCell<T>> 组合实现"多所有者 + 可修改";
- Rc 强引用互相指向会形成引用循环、造成内存泄漏;
Weak<T> 弱引用不增 strong_count、不拥有数据,upgrade 后才能访问;树中父强子弱、子用 Weak 指回;
- Copy 小类型可用更轻的
Cell<T>。
八、动手练习
- 用 Rc 实现三个链表共享同一段尾部,打印各阶段 strong_count;
- 用
Rc<RefCell<i32>> 创建一个共享计数器,从两个不同所有者处递增;
- 故意制造两个 borrow_mut 同时活跃,观察运行时 panic 信息;
- 把树示例中 parent 改成 Rc(强引用),分析为何会泄漏,再改回 Weak;
- (选做)用 Rc/RefCell 实现一个简单的有向图,支持添加边,并从任意节点遍历。
下篇预告:第 18 篇|无畏并发
学完单线程的智能指针,下一篇我们进入多线程世界。Rust 凭借所有权、Send/Sync 两个标记 trait,在编译期保证"能编译就没有数据竞争"。届时将学习创建线程、消息传递(channel)、共享状态(Mutex + Arc),并理解 Send/Sync 的自动判定。
更多 Rust 编程学习资源与开发者交流,欢迎访问 云栈社区。