趣味学RUST基础篇(智能指针_结束)
接下来,我们用一个生动的故事带你轻松理解 Rust 中的 内部可变性(Interior Mutability) 和 RefCell<T>
!
《表面“安静”,内心“狂野”》——Rust 的 RefCell<T>
反叛记
打个比方,你是个严格的图书管理员,图书馆有条铁律:“书一旦借出,就不能再改!”
- 如果你只“读”这本书,可以借给很多人。
- 但如果你想“改”它,必须独占,别人谁也不能看。
这就是 Rust 的借用规则:同一时间,要么多个只读引用,要么一个可变引用,不能同时存在。听起来很合理,对吧?但有时候……我们想搞点“小动作”。
有个“表面乖巧,内心想改”的家伙:RefCell<T>
Rust 有个叫 RefCell<T>
的类型,它就像一个会装乖的调皮学生。它对外说:“我是个不可变的变量,大家放心用我!”。但背地里却偷偷摸摸地修改自己的内容——这就是 内部可变性(Interior Mutability)。
外表不可变,内心可变 —— 这就是“内部可变性”模式。
但 Rust 是安全至上的语言,怎么可能允许这种“欺骗”行为?答案是:它不是欺骗,而是“合法越界”。
RefCell<T>
在运行时检查借用规则(而不是编译时),一旦你违规,它就会当场 panic 报警,而不是让你编译通过后出错。
举个例子:测试中的“消息记录员”
假设我们写了一个“限额提醒系统”——比如你每月只能打 100 个电话,快超了就提醒你。系统本身不负责发消息(短信、邮件等),它只“通知”一个叫 Messenger
的东西去发。我们想测试这个系统是否在正确时机“说”了正确的话。于是我们造了个“假人”——MockMessenger
,它不真发消息,只偷偷记下来:
struct MockMessenger {sent_messages: Vec<String>, // 记录发了啥
}
然后我们实现一个 send
方法:
impl Messenger for MockMessenger {fn send(&self, msg: &str) {self.sent_messages.push(msg.to_string()); // 想记录消息}
}
但编译器立刻跳出来大喊:“不行!self
是 &self
,不可变!你不能 push!”
因为 send
是 trait 方法,定义是:
trait Messenger {fn send(&self, msg: &str); // 参数是 &self,不能改自己!
}
我们不能改接口,否则所有用户代码都得改。怎么办?难道让测试失败?
破局神器:RefCell<T>
登场!
这时候,RefCell<T>
就像一个“可变保险箱”出场了。
我们把 sent_messages
装进 RefCell
:
struct MockMessenger {sent_messages: RefCell<Vec<String>>, // 包一层 RefCell
}
然后在 send
方法里,我们“借”出可变权限:
fn send(&self, msg: &str) {self.sent_messages.borrow_mut().push(msg.to_string());// borrow_mut():我要改里面的内容!
}
虽然 self
是不可变的,但 RefCell
说:
“外面不变没关系,里面的东西我来管!你要改?行,我借你权限,但得按规矩来!”
运行时检查 vs 编译时检查
类型 | 检查时机 | 违规后果 |
---|---|---|
&mut / Box<T> | 编译时 | 编译失败 |
RefCell<T> | 运行时 | 程序 panic |
举个例子:
let mut one = self.sent_messages.borrow_mut();
let mut two = self.sent_messages.borrow_mut(); // 同时两个可变借用!
编译没问题,但一运行就 panic:
thread 'main' panicked at 'already borrowed: BorrowMutError'
就像保安发现两个人同时拿着“修改钥匙”想进房间,立马拉响警报!
RefCell<T>
+ Rc<T>
= 共享 + 可变
前面我们学了 Rc<T>
,它能让多个所有者共享数据,但只能读,不能改。
那如果我想多个地方共享,还能修改呢?
答案:Rc<RefCell<T>>
!
就像一个“共享保险箱”,大家都能打开,还能往里写东西。
举个例子:我们有个链表,多个列表共享一个节点,还想改里面的值:
use std::rc::Rc;
use std::cell::RefCell;let value = Rc::new(RefCell::new(5)); // 5 被装进“可变共享保险箱”let a = Rc::new(Cons(Rc::clone(&value), Nil));
let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));// 现在我们改 value!
*value.borrow_mut() += 10; // 值变成 15!println!("a = {:?}", a); // a 也看到 15 了!
成功!a
、b
、c
都看到了更新后的值!
一句话总结
RefCell<T>
是 Rust 的“内部可变性”工具,它让你在“外表不可变”的情况下,安全地修改内部数据 —— 规则检查从“编译时”挪到“运行时”。
它适合这些场景:
- 写测试 mock 对象
- 构建循环数据结构
- 多个所有者共享并修改同一数据(配合
Rc<T>
)
类比小剧场
现实场景 | Rust 对应 |
---|---|
图书馆规定不能涂改书籍 | Rust 的默认借用规则 |
学生偷偷用铅笔在草稿本上写写画画 | RefCell<T> 的内部可变性 |
老师发现后当场批评 | RefCell 在运行时 panic |
多人共用一个笔记本,轮流写 | Rc<RefCell<T>> |
你不能改书,但可以改“读书笔记” | send(&self) 中修改 RefCell 内容 |
注意事项
RefCell<T>
只用于单线程(多线程用Mutex<T>
)- 它有运行时开销(计数、检查)
- 它不能避免逻辑错误,但能防止内存安全问题
所以你看,RefCell<T>
不是“破坏规则”,而是 在安全的前提下,给你更多灵活性。
它就像一个戴着“不可变”面具的忍者,表面不动声色,内心却能悄然改变世界。
当然可以!下面是对 https://kaisery.github.io/trpl-zh-cn/ch15-06-reference-cycles.html 的趣味化、通俗易懂版本转写,用一个生动的故事带你轻松理解 Rust 中的 引用循环(Reference Cycles) 和如何用 Weak<T>
打破它!
Rust 的“引用循环”危机
你有没有过这样的经历,你和你最好的朋友一起进了一间密室,门在你们身后自动关上了。
但你们发现,开门的规则是:
“只有当另一个人离开后,你才能走。”
于是你对朋友说:“你先走!”
朋友说:“不不不,你先请!”
你又说:“还是你先吧!”
……
就这样,你们一直互相谦让,谁也没能走出去。最终——饿死在里面了 ,^_^
这,就是编程世界里的 “引用循环”(Reference Cycle) —— 两个对象互相持有对方,导致谁都无法被释放,内存“卡住”了。
今天,我们就来揭开这个“内存困局”的真相,并学会用 Rust 的“弱引用”武器把它打破!
引用循环是怎么发生的?
还记得我们之前学的 Rc<T>
吗?它是“引用计数”智能指针,用来实现多个所有者共享数据。
比如:
let a = Rc::new("小明");
let b = Rc::clone(&a); // 计数变成2
当没人再用时,计数归零,数据就被释放。但问题来了:如果两个 Rc<T>
互相指向对方,会发生什么?
举个例子:两个列表互相指向
假设我们有两个链表节点 a
和 b
:
a
指向b
b
也指向a
代码大概是这样:
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));// 然后让 a 指向 b
* a.tail().borrow_mut() = Rc::clone(&b);
这时候,引用计数变成了:
a
被b
和主函数使用 → 计数 = 2b
被a
和主函数使用 → 计数 = 2
当程序结束时:
- 先释放
b
→ 计数从 2 变成 1(因为a
还指着它) - 再释放
a
→ 计数从 2 变成 1(因为b
还指着它)
结果:两个都没法彻底释放!内存泄漏了!
就像那两个互相谦让的朋友,谁也不肯先走,最后都“卡”在内存里,永远无法回收。
如图所示
引用循环的危害
- 内存越占越多,程序变慢
- 长时间运行可能耗尽内存
- 虽然 Rust 很安全,但
Rc<T>
+RefCell<T>
组合不当,也可能“翻车”
所以记住:
Rust 不保证完全防止内存泄漏。引用循环是程序员的逻辑错误,编译器不会帮你抓!
破局之道:用 Weak<T>
打破循环!
怎么解决?我们需要一种“不增加引用计数的引用”——这就是 Weak<T>
,中文叫 弱引用。
Weak<T>
是什么?
- 它像一个“只看不拥有的眼神”
- 它指向某个
Rc<T>
,但不算“拥有者” - 它不会增加 strong_count(强引用计数),只增加 weak_count(弱引用计数)
- 当强引用为 0 时,即使还有弱引用,数据也会被释放
你可以把 Weak<T>
想象成:
“我认识你,但我不是你的主人。你走了,我也就看不见了。”
实战案例:父子树结构
想象一棵树,有父节点和子节点:
- 父节点拥有子节点(强引用)
- 子节点想“知道”父节点是谁,但不能拥有父节点
否则就会出现循环:
父节点 → 子节点 → 父节点 → 子节点 → …
解决方案:子节点用 Weak<T>
指向父节点
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![]),});println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());let branch = Rc::new(Node {value: 5,parent: RefCell::new(Weak::new()),children: RefCell::new(vec![Rc::clone(&leaf)]),});// 让 leaf 知道它的父节点是 branch(用弱引用)*leaf.parent.borrow_mut() = Rc::downgrade(&branch);// 注意:这里是 downgrade,不是 clone!println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}
Rc::downgrade
vs Rc::clone
方法 | 作用 | 计数变化 |
---|---|---|
Rc::clone(&x) | 创建强引用 | strong_count += 1 |
Rc::downgrade(&x) | 创建弱引用 | weak_count += 1 |
弱引用如何避免循环?
当 branch
离开作用域时:
- 它的
strong_count
减 1 - 如果变为 0,即使
leaf.parent
还指着它,它也会被释放 Weak<T>
不会阻止释放!
就像:
“我知道我爸是谁,但他走了,我也不能把他拉回来。”
如何使用 Weak<T>
的值?
因为 Weak<T>
指向的对象可能已经被释放了,所以不能直接用。
你需要先“升级”成 Rc<T>
,并检查是否还有效:
if let Some(parent) = leaf.parent.borrow().upgrade() {println!("父节点值:{}", parent.value);
} else {println!("父节点已经没了!");
}
upgrade()
返回Option<Rc<T>>
- 如果原数据还在 →
Some(Rc<T>)
- 如果已被释放 →
None
安全又可靠!
引用计数的变化演示
let leaf = Rc::new(Node::new(3));
println!("leaf 强引用: {}, 弱引用: {}", Rc::strong_count(&leaf), Rc::weak_count(&leaf));
// 输出:1, 0{let branch = Rc::new(Node::new(5));*leaf.parent.borrow_mut() = Rc::downgrade(&branch);println!("branch 强引用: {}, 弱引用: {}", Rc::strong_count(&branch), Rc::weak_count(&branch));// 输出:1, 1 (因为 leaf.parent 是弱引用)// branch 离开作用域,强引用归零,被释放
}// 此时再查 leaf.parent,upgrade() 会返回 None
println!("leaf.parent: {:?}", leaf.parent.borrow().upgrade()); // None
完美!没有内存泄漏!
一句话总结
引用循环就像两个朋友互相谦让,结果谁也出不去。用
Weak<T>
打破循环,让一方“只看不拥有”,就能顺利释放内存。
使用建议
- 当你需要双向引用时,让“从属方”使用
Weak<T>
- 常见场景:树结构中的父子关系、图结构中的反向边
- 多用测试和代码审查,避免意外形成引用循环
类比小剧场
现实场景 | Rust 对应 |
---|---|
两人互相让门 | 强引用循环 |
一人说“你先走”,另一人只是看着 | 弱引用打破循环 |
监控摄像头记录谁在场 | strong_count 和 weak_count |
问保安:“那人还在吗?” | upgrade() 检查有效性 |
现在你已经掌握了 Rust 中最 tricky 的内存陷阱之一,并学会了如何用 Weak<T>
安全化解!
记住:Rust 允许你犯错,但也会给你工具去纠正它。