ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

深入理解 Rust 内部可变性(Interior Mutability):从 `Sender::send(self)` 到 `Rc` 与 `RefCell`

深入理解 Rust 内部可变性(Interior Mutability):从 `Sender::send(self)` 到 `Rc` 与 `RefCell` 示例工程教程【免费下载链接】100-exercises-to-learn-rustA self-paced course to learn Rust, one exercise at a time.项目地址https://gitcode.com/GitHub_Trending/10/100-exercises-to-learn-rust点击查看免费下载本文以 100-exercises-to-learn-rust 课程中线程章节07_threads的配套讲义 book/src/07_threads/06_interior_mutability.md 为核心从标准库通道Sender::send(self)的签名出发讲清共享引用并不等于不可变引用这一 Rust 关键心智模型。读完本文你将掌握内部可变性interior mutability的定义、其底层基石UnsafeCell的作用以及Rc、RefCell两个标准库典型实现并能独立完成配套练习DropTracker的实现。一个耐人寻味的签名send(self)如何改变通道状态在课程的通道章节book/src/07_threads/05_channels.md中我们搭建了一个客户端-服务器架构一个长期运行的服务器线程管理票据状态多个客户端线程通过std::sync::mpsc通道向它发送命令。其中Sender的send方法签名如下implT SenderT { pub fn send(self, t: T) - Result(), SendErrorT { // [...] } }这里存在一个明显的矛盾send接收的是self共享引用但它显然在改变通道的状态——把一条新消息推进通道内部队列。更关键的是Sender是可克隆的多个客户端线程可以各自持有一份Sender克隆在同一时刻并发地向同一个通道写入数据。这正是支撑整个客户端-服务器架构的核心性质。但问题也随之而来它为什么能工作这不是违反了 Rust 的借用规则吗我们是怎样通过一个不可变引用完成变更的答案就是本讲的主题内部可变性interior mutability。更准确的命名共享引用T与独占引用mut T在借位检查器borrow-checker章节中我们习惯把两种引用称为不可变引用T可变引用mut T但如果追求精确更准确的叫法应当是共享引用shared referencesT独占引用exclusive referencesmut T不可变/可变这套心智模型在绝大多数场景下都成立也是初学 Rust 时最实用的抓手。但它并不是故事的全部——正如send(self)所示T实际上并不保证其指向的数据不可变。不必担心Rust 依然信守承诺。只是这些术语的含义比乍看起来更微妙当多个线程共享同一份数据时Rust 需要一种机制让共享访问与内部变更能够共存而不破坏借用规则。内部可变性正是这把钥匙。UnsafeCell告诉编译器这个共享引用确实可变Rust 编译器的默认假设是共享引用指向的数据是不可变的。基于这一假设编译器会大胆优化——重排操作、缓存取值、做各种让代码更快的小动作。当你需要推翻这个假设时标准库提供了底层工具UnsafeCell把数据包进UnsafeCell就等于告诉编译器不这个共享引用实际是可变的。因此凡是允许通过共享引用修改数据的类型其内部必然直接或间接地含有UnsafeCell——这是识别内部可变性类型的一个可靠判据。借助UnsafeCell、裸指针raw pointers和unsafe代码你就可以通过共享引用修改数据。但必须澄清UnsafeCell并不是让你无视借位检查器的魔法棒unsafe代码依然受 Rust 借用与别名aliasing规则的约束。它只是一个进阶的工具用于构建类型系统无法直接表达其安全性的安全抽象。每当你写出unsafe关键字你实际上是在对编译器说我知道我在做什么我不会违反你的不变量请相信我。这也意味着每次调用unsafe函数其文档都会说明安全前置条件safety preconditions——在什么情况下执行其中的unsafe块是安全的。UnsafeCell自身的前置条件收录在std文档中。本课程不会直接使用UnsafeCell也不会编写unsafe代码但理解它为什么存在、与日常使用的类型有什么关系依然非常重要——它是所有内部可变性类型的地基。标准库中的关键实例让我们考察两个std中借助内部可变性实现的类型。它们在日常 Rust 代码中相当常见尤其是当你翻阅某些库的实现细节时。引用计数RcRcreference-counted pointer是一个引用计数指针。它包裹一个值并记录当前存在多少引用当最后一个引用被丢弃时值才被释放。被Rc包裹的值本身是不可变的你只能获得对它的共享引用。真正的可变性发生在引用计数上use std::rc::Rc; let a: RcString Rc::new(My string.to_string()); // 目前只有一份对字符串数据的引用。 assert_eq!(Rc::strong_count(a), 1); // 调用 clone 时字符串数据并没有被复制 // 相反Rc 的引用计数被递增。 let b Rc::clone(a); assert_eq!(Rc::strong_count(a), 2); assert_eq!(Rc::strong_count(b), 2); // ^ a 和 b 指向同一份字符串数据 // 并且共享同一个引用计数器。Rc在内部使用UnsafeCell让共享引用得以安全地递增、递减引用计数——这正是内部可变性的典型应用。需要留意的是Rc是单线程使用的引用计数指针并不跨线程共享在多线程场景下需要换成Arc原子引用计数这一点在后续线程相关课程中会进一步展开。RefCell运行时借用检查RefCell是 Rust 中最常见的内部可变性示例之一即使你只持有对RefCell本身的不可变引用也可以修改其中包裹的值。实现机制是运行期借用检查runtime borrow checkingRefCell在运行时跟踪当前对内部值的引用数量与类型。如果在已经存在不可变借用的情况下再尝试可变借用程序会直接 panic从而保证 Rust 的借用规则始终被强制执行——只是把检查从编译期推迟到了运行期use std::cell::RefCell; let x RefCell::new(42); let y x.borrow(); // 不可变借用 let z x.borrow_mut(); // Panics! 已存在活跃的不可变借用。这段代码编译是可以通过的但运行到borrow_mut()时会触发 panic因为运行时发现存在活跃的不可变借用。这正是编译期检查 vs 运行期检查两种策略的取舍RefCell用运行期代价换取更灵活的使用方式。动手实践用RcRefCellT实现DropTracker课程为这一讲配备了配套练习 exercises/07_threads/06_interior_mutability/src/lib.rs要求使用Rc和RefCell实现DropTrackerT——一个包装T类型值的结构体每次被包装的值被丢弃drop时递增一个共享的usize计数器。练习骨架如下use std::cell::RefCell; use std::rc::Rc; pub struct DropTrackerT { value: T, counter: todo!(), } implT DropTrackerT { pub fn new(value: T, counter: todo!()) - Self { Self { value, counter } } } implT Drop for DropTrackerT { fn drop(mut self) { todo!() } }结合本讲知识counter字段的类型应为RcRefCellusizeRc让多个DropTracker实例共享同一份计数器数据对应共享引用RefCell则允许我们在只有共享引用时也能修改计数器对应内部可变性。drop实现中通过*counter.borrow_mut() 1完成递增。配套测试位于同一文件的tests模块内恰好验证了这一设计#[test] fn it_works() { let counter Rc::new(RefCell::new(0)); let _ DropTracker::new((), Rc::clone(counter)); assert_eq!(*counter.borrow(), 1); } #[test] fn multiple() { let counter Rc::new(RefCell::new(0)); { let a DropTracker::new(5, Rc::clone(counter)); let b DropTracker::new(6, Rc::clone(counter)); } assert_eq!(*counter.borrow(), 2); }可以看到it_works中DropTracker::new接收Rc::clone(counter)只复制指针、递增计数不复制数据当临时值被丢弃时计数器变为 1multiple中两个DropTracker在作用域结束后被依次丢弃共享计数器变为 2。这正是多个共享引用 单一可变状态的经典组合。该练习的Cargo.tomlexercises/07_threads/06_interior_mutability/Cargo.toml声明了独立的interior_mutability包在仓库根目录运行cargo test -p interior_mutability即可执行上述测试。练习同样以todo!()占位完成实现后测试将从 panic 转为通过。回到通道内部可变性在客户端-服务器架构中的位置理解了内部可变性之后我们就能完整解释开篇的问题std::sync::mpsc::Sender的send(self)之所以能通过共享引用推进消息入队正是因为其内部实现借助了UnsafeCell系的同步原语channel 的底层缓冲与状态被封装在可安全共享的结构中从而在不违背借用规则的前提下支持多线程并发写入。这一点在课程代码中有着清晰的呼应。在 exercises/07_threads/05_channels/src/lib.rs 中launch()创建通道后把Receiver移动进服务器线程向调用方返回SenderCommandSender可以被任意数量的客户端线程克隆共享。而后续 exercises/07_threads/08_client/src/lib.rs 中可以看到完整形态服务器线程在loop里receiver.recv()等待命令独占地拥有TicketStorelet mut store TicketStore::new()客户端则通过持有共享的Sender克隆来并发提交Insert/Get命令并通过响应通道取回结果。这套架构之所以成立正是建立在共享引用 内部可变性这一基础之上——self的send让并发客户端可以共享地、安全地向同一通道写入。延伸内部可变性的家族与选型理解了UnsafeCell→Rc/RefCell这条主线后你会更容易理解标准库中其他共享引用可变的类型族单线程组合Rc共享所有权RefCell运行期借用检查如本讲练习所示Cell提供对Copy类型的简单内部可变性。多线程组合Arc原子引用计数Mutex/RwLock互斥锁与读写锁把借用检查转化为加锁/阻塞以及std::sync::atomic中的原子类型这些会在后续线程课程07_threads 的 locks、rw_lock、sync 等章节中逐一登场。惰性初始化OnceLock、LazyLock等类型同样依赖内部可变性实现首次写入、此后只读的语义。选型要点很朴素能用编译期借用检查解决的就不要引入运行期检查或锁RefCell把借用冲突推迟到运行期以 panic 为代价Mutex则把冲突转化为线程阻塞——二者都是安全但有代价的权衡。小结本讲澄清了 Rust 中最重要的心智模型修正之一T应称为共享引用而非不可变引用它不保证数据不可变内部可变性 通过共享引用修改数据的能力其底层基石是UnsafeCellRc通过UnsafeCell在共享引用下维护引用计数RefCell用运行期借用检查换取灵活的可变性本课程不使用unsafe但理解这些底层机制有助于读懂你所依赖的每一个标准库类型。掌握了内部可变性你就能解释Sender::send(self)的魔法也能在需要多份共享引用 一处可变状态时自信地组合Rc与RefCell单线程或Arc与Mutex多线程。赞分享示例工程教程【免费下载链接】100-exercises-to-learn-rustA self-paced course to learn Rust, one exercise at a time.项目地址https://gitcode.com/GitHub_Trending/10/100-exercises-to-learn-rust点击查看免费下载相关推荐comprehensive-rust 深入解析Rust 内部可变性Interior Mutability——用 Cell 与 RefCell 在共享引用背后安全修改数据comprehensive rust 深入解析Rust 内部可变性Interior Mutability——用 Cell 与 RefCell 在共享引用背文档教程Rust 智能指针与内部可变性实战从 C/C 内存管理到 BoxT、RcT、CellT/RefCellTRust 智能指针与内部可变性实战从 C/C 内存管理到 BoxT 、 RcT 、 CellT / RefCellT 本文导读 本文以 Rust文档教程Pop GTK主题的国际化与本地化多语言支持实现指南Pop GTK主题的国际化与本地化多语言支持实现指南 Pop GTK主题作为System76开发的Linux桌面环境主题以其现代化设计和流畅体验受到众多上一篇HS2-HF Patch 新手完整上手指南一次安装打通 Honey Select 2 汉化、去码与插件下一篇Linux 压缩解压实战tar、gzip、zip 常用命令速查清单创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表