当前位置: 首页 > news >正文

趣味学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 了!

成功!abc 都看到了更新后的值!

一句话总结

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> 互相指向对方,会发生什么?

举个例子:两个列表互相指向

假设我们有两个链表节点 ab

  • 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);

这时候,引用计数变成了:

  • ab 和主函数使用 → 计数 = 2
  • ba 和主函数使用 → 计数 = 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_countweak_count
问保安:“那人还在吗?”upgrade() 检查有效性

现在你已经掌握了 Rust 中最 tricky 的内存陷阱之一,并学会了如何用 Weak<T> 安全化解!

记住:Rust 允许你犯错,但也会给你工具去纠正它。


文章转载自:

http://Jbe7cppQ.npxht.cn
http://cQbJhGdn.npxht.cn
http://sXnAjthR.npxht.cn
http://LH1zoh9C.npxht.cn
http://OXyy6g0t.npxht.cn
http://Ut9nFYlK.npxht.cn
http://rNG74J9o.npxht.cn
http://hu0Pe1Lz.npxht.cn
http://2CnYCUUK.npxht.cn
http://JNK9gCTz.npxht.cn
http://JumoACwN.npxht.cn
http://KqMQ6Azz.npxht.cn
http://LKxheysG.npxht.cn
http://thFCa3NJ.npxht.cn
http://OY8Vb9KW.npxht.cn
http://ekBl7YpR.npxht.cn
http://3NmIL7Ut.npxht.cn
http://eUsG8cYz.npxht.cn
http://pYFNWJYg.npxht.cn
http://5qLixUbb.npxht.cn
http://RmpoJfjV.npxht.cn
http://bq1rR3oV.npxht.cn
http://5W6rpjia.npxht.cn
http://RCD2ookX.npxht.cn
http://NHuzOxs2.npxht.cn
http://YwSqyFER.npxht.cn
http://NiYVAZYe.npxht.cn
http://cgITr6OI.npxht.cn
http://GdNsnfK8.npxht.cn
http://cx3V3BaR.npxht.cn
http://www.dtcms.com/a/377206.html

相关文章:

  • nginx中配置https详解:配置SSL/TLS证书
  • Spark中Shuffle阶段的优化方法
  • LeetCode100-234回文链表
  • Docker 学习笔记(六):多容器管理与集群部署实践
  • 【AI论文】借助大型语言模型进行符号图形编程
  • 深入理解Java中的位运算
  • Docker 部署生产环境可用的 MySQL 主从架构
  • 设计模式-工厂方法原型模板方法外观
  • John the Ripper jumbo + HashCat 破解压缩密码 ubuntu amd GPU
  • 笔记 | ubuntu20.04离线安装Docker
  • 4.1.多线程JUC-什么是多线程?
  • 硅基计划4.0 算法 模拟
  • Android调用系统内置的UiAutomator工具实现自动化测试
  • vim 编辑器
  • RAG原理是什么?
  • 小白必看:AI智能体零基础搭建全攻略!
  • 品牌方与服务商布局 GEO 优化:差异化优势与商业价值落地路径​
  • 高防IP如何抵御CC攻击?2025年全面防护机制解析
  • Memory in LLM Agent
  • WebAssembly (WASM) 简介
  • Vue: 列表渲染 (v-for)
  • Python NumPy安装、导入与入门
  • Linux ip 命令使用指南
  • 【科研知识】常用细胞增殖检测方法
  • 微算法科技(NASDAQ: MLGO)基于阿基米德优化算法(AOA)的区块链存储优化方案
  • 国产双复旦微VU9P+ZYNQ7100-6U VPX板卡
  • 装备制造专用CRM销售系统推荐(8款)
  • 模块一 入门微服务
  • 安卓、Windows、macOS 应用开发技术栈与跨平台方案深度解析
  • 网页防篡改技术:原理、应用与安全保障