趣味学RUST基础篇(智能指针_结束)

接下来,我们用一个生动的故事带你轻松理解 Rust 中的 内部可变性(Interior Mutability)RefCell<T>

《表面"安静",内心"狂野"》------Rust 的 RefCell<T> 反叛记

打个比方,你是个严格的图书管理员,图书馆有条铁律:"书一旦借出,就不能再改!"

  • 如果你只"读"这本书,可以借给很多人。
  • 但如果你想"改"它,必须独占,别人谁也不能看。

这就是 Rust 的借用规则:同一时间,要么多个只读引用,要么一个可变引用,不能同时存在。听起来很合理,对吧?但有时候......我们想搞点"小动作"。

有个"表面乖巧,内心想改"的家伙:RefCell<T>

Rust 有个叫 RefCell<T> 的类型,它就像一个会装乖的调皮学生 。它对外说:"我是个不可变的变量,大家放心用我!"。但背地里却偷偷摸摸地修改自己的内容------这就是 内部可变性(Interior Mutability)

外表不可变,内心可变 ------ 这就是"内部可变性"模式。

但 Rust 是安全至上的语言,怎么可能允许这种"欺骗"行为?答案是:它不是欺骗,而是"合法越界"

RefCell<T> 在运行时检查借用规则(而不是编译时),一旦你违规,它就会当场 panic 报警,而不是让你编译通过后出错。

举个例子:测试中的"消息记录员"

假设我们写了一个"限额提醒系统"------比如你每月只能打 100 个电话,快超了就提醒你。系统本身不负责发消息(短信、邮件等),它只"通知"一个叫 Messenger 的东西去发。我们想测试这个系统是否在正确时机"说"了正确的话。于是我们造了个"假人"------MockMessenger,它不真发消息,只偷偷记下来:

rust 复制代码
struct MockMessenger {
    sent_messages: Vec<String>, // 记录发了啥
}

然后我们实现一个 send 方法:

rust 复制代码
impl Messenger for MockMessenger {
    fn send(&self, msg: &str) {
        self.sent_messages.push(msg.to_string()); // 想记录消息
    }
}

但编译器立刻跳出来大喊:"不行!self&self,不可变!你不能 push!"

因为 send 是 trait 方法,定义是:

rust 复制代码
trait Messenger {
    fn send(&self, msg: &str); // 参数是 &self,不能改自己!
}

我们不能改接口,否则所有用户代码都得改。怎么办?难道让测试失败?


破局神器:RefCell<T> 登场!

这时候,RefCell<T> 就像一个"可变保险箱"出场了。

我们把 sent_messages 装进 RefCell

rust 复制代码
struct MockMessenger {
    sent_messages: RefCell<Vec<String>>, // 包一层 RefCell
}

然后在 send 方法里,我们"借"出可变权限:

rust 复制代码
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

举个例子:

rust 复制代码
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>>

就像一个"共享保险箱",大家都能打开,还能往里写东西。

举个例子:我们有个链表,多个列表共享一个节点,还想改里面的值:

rust 复制代码
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> 吗?它是"引用计数"智能指针,用来实现多个所有者共享数据。

比如:

rust 复制代码
let a = Rc::new("小明");
let b = Rc::clone(&a); // 计数变成2

当没人再用时,计数归零,数据就被释放。但问题来了:如果两个 Rc<T> 互相指向对方,会发生什么?

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

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

  • a 指向 b
  • b 也指向 a

代码大概是这样:

rust 复制代码
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> 指向父节点

rust 复制代码
use std::cell::RefCell;
use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Node {
    value: i32,
    parent: RefCell<Weak<Node>>,// 弱引用,不增加计数
    children: RefCell<Vec<Rc<Node>>>,// 强引用,拥有孩子
}

创建节点:

rust 复制代码
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>,并检查是否还有效:

rust 复制代码
if let Some(parent) = leaf.parent.borrow().upgrade() {
    println!("父节点值:{}", parent.value);
} else {
    println!("父节点已经没了!");
}
  • upgrade() 返回 Option<Rc<T>>
  • 如果原数据还在 → Some(Rc<T>)
  • 如果已被释放 → None

安全又可靠!

引用计数的变化演示

rust 复制代码
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 允许你犯错,但也会给你工具去纠正它。

相关推荐
爱编程的化学家3 小时前
代码随想录算法训练营第六天 - 哈希表2 || 454.四数相加II / 383.赎金信 / 15.三数之和 / 18.四数之和
数据结构·c++·算法·leetcode·双指针·哈希
CVer儿3 小时前
qt资料2025
开发语言·qt
DevilSeagull3 小时前
JavaScript WebAPI 指南
java·开发语言·javascript·html·ecmascript·html5
2zcode4 小时前
基于Matlab不同作战类型下兵力动力学模型的构建与稳定性分析
开发语言·matlab
闲人编程5 小时前
图像去雾算法:从物理模型到深度学习实现
图像处理·人工智能·python·深度学习·算法·计算机视觉·去雾
葵野寺5 小时前
【RelayMQ】基于 Java 实现轻量级消息队列(七)
java·开发语言·网络·rabbitmq·java-rabbitmq
咔咔学姐kk5 小时前
大模型微调技术宝典:Transformer架构,从小白到专家
人工智能·深度学习·学习·算法·transformer
zyx没烦恼6 小时前
Qt 基础编程核心知识点全解析:含 Hello World 实现、对象树、坐标系及开发工具使用
开发语言·qt
木心爱编程6 小时前
C++链表实战:STL与手动实现详解
开发语言·c++·链表