ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Rust 智能指针之 `Rc<T>`:引用计数与多所有权共享实战(TRPL 第 15 章)

Rust 智能指针之 `Rc<T>`:引用计数与多所有权共享实战(TRPL 第 15 章) 教程文档【免费下载链接】bookThe Rust Programming Language项目地址https://gitcode.com/gh_mirrors/bo/book点击查看免费下载导读RcTReference Counting引用计数是 Rust 标准库中用于实现多所有权的核心智能指针它允许同一个堆上值被程序的多个部分同时持有所有权并通过计数决定何时安全回收。本篇文章基于《The Rust Programming Language》开源仓库的 src/ch15-04-rc.md 章节以经典的 cons list 链表为例完整演示如何用RcT替换BoxT解决一个值被多处共享的编译期所有权冲突并配合仓库中的可运行代码与输出讲透引用计数增减、Rc::clone与深拷贝clone的本质区别以及RcT为何只能用于单线程场景。读完你不仅能正确使用RcT还能理解它与后续RefCellT组合使用的缘由。什么是多所有权为什么单个值需要多个拥有者在大多数情况下Rust 的所有权模型清晰明确你能准确说出某个值归哪个变量所有。但现实中存在一种场景——同一个值可能有多个拥有者。最典型的例子是图graph数据结构多条边edge可能指向同一个节点node从概念上讲这个节点同时被指向它的所有边共同拥有。只有当不再有任何边指向该节点即它没有任何拥有者时它才能被安全清理。RcT正是为这种需求设计的标准库类型。它维护着一个对这个值有多少个引用的计数器当引用数为0时说明值已不再被使用可以安全清理且不会产生悬垂引用当引用数大于 0 时值保持有效。一个形象的类比RcT像客厅里的一台电视机。第一个人进屋时打开电视其他人可以陆续进来一起看只有当最后一个人离开房间时电视才会被关掉。如果有人还在看电视就把电视关了剩下的观众必然抗议——这和引用计数不足时提前释放数据导致的未定义行为如出一辙。何时该用RcT书中给出了明确的使用判据当你要在堆上分配数据供程序多个部分只读共享并且无法在编译期确定哪个部分会最后用完该数据时就用RcT。反过来说如果你能提前知道谁最后用完数据直接让那一方成为数据的唯一拥有者编译期的常规所有权规则就能生效无需RcT。单线程限定RcT只能用于单线程场景。它内部使用非原子的计数器在多线程下会产生数据竞争。多线程环境下的引用计数需要原子类型ArcT在第 16 章并发部分讲解仓库的 src/ch16-00-concurrency.md 一章对此有专门展开。共享数据实战两个链表共享第三个链表的所有权让我们回到书中第 15 章的 cons list 例子。回忆 Listing 15-5它用BoxT定义了递归的链表类型enum List { Cons(i32, BoxList), Nil, } use crate::List::{Cons, Nil}; fn main() { let list Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil)))))); }现在我们要创建两个链表b和c让它们共同拥有第三个链表a的所有权。结构如下图所示图 15-3链表b以元素 3 开头链表c以元素 4 开头而两者都指向链表a的首元素5 → 10 → Nil。用BoxT实现为什么失败E0382如果我们沿用BoxList的定义代码会写成 Listing 15-17 这样enum List { Cons(i32, BoxList), Nil, } use crate::List::{Cons, Nil}; fn main() { let a Cons(5, Box::new(Cons(10, Box::new(Nil)))); let b Cons(3, Box::new(a)); let c Cons(4, Box::new(a)); }编译时立即报错仓库中对应的错误输出 listing-15-17/output.txt 显示$ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list) error[E0382]: use of moved value: a -- src/main.rs:11:30 | 9 | let a Cons(5, Box::new(Cons(10, Box::new(Nil)))); | - move occurs because a has type List, which does not implement the Copy trait 10 | let b Cons(3, Box::new(a)); | - value moved here 11 | let c Cons(4, Box::new(a)); | ^ value used here after move原因很清楚Cons变体拥有它所持有的数据。创建b时a被移动进了bb成为a的唯一拥有者再想用a创建c时a已经被移走编译器直接拒绝。这也是所有权模型最典型的价值——在编译期就拦住数据被双重拥有导致的隐患。改用引用 生命周期行不行另一种思路是把Cons改成持有引用但这会引入生命周期参数且意味着链表中的每个元素都要活得和整个链表一样长。这在 Listing 15-17 的场景里恰好成立但在很多真实场景中并不满足例如节点在运行时才动态创建、随时可能被独立释放。因此更通用的方案是换用RcT。用RcT重写Rc::clone让引用计数从 1 涨到 3将List定义中的BoxT替换为RcT即可解决共享问题见 Listing 15-18enum List { Cons(i32, RcList), Nil, } use crate::List::{Cons, Nil}; use std::rc::Rc; fn main() { let a Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); let b Cons(3, Rc::clone(a)); let c Cons(4, Rc::clone(a)); }关键变化有三点引入RcTRcT不在标准库 prelude 中必须显式use std::rc::Rc;引入用Rc::new构造a的数据被放进堆上的RcList中a本身是一个RcList用Rc::clone(a)共享创建b、c时不再把a移动走而是克隆a持有的RcList。每次调用Rc::clone指向同一份数据的引用计数就加 1创建a后计数为 1创建b后为 2创建c后为 3。只有当计数降到 0 时RcList里的数据才会被清理。从该示例的 Cargo.toml 可以看到这是一个名为cons-list、edition 2024的标准 Cargo 二进制项目没有任何第三方依赖你可以直接cargo run验证。为什么是Rc::clone(a)而不是a.clone()你完全可以写a.clone()但 Rust 社区的约定是在引用计数场景显式调用Rc::clone。原因很实际大多数类型的clone实现是深拷贝复制全部数据开销大Rc::clone的实现只递增引用计数不做任何数据深拷贝开销极小混用两种clone会让人分不清哪一次是昂贵的深拷贝、哪一次只是计数1。统一写Rc::clone在做性能排查时就能一眼跳过这些廉价调用只聚焦真正的深拷贝点。观察引用计数变化Rc::strong_count光说不练不行Listing 15-19 在main中增加了一个内部作用域包裹链表c并在每个计数变化点调用Rc::strong_count(a)打印当前强引用计数enum List { Cons(i32, RcList), Nil, } use crate::List::{Cons, Nil}; use std::rc::Rc; fn main() { let a Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); println!(count after creating a {}, Rc::strong_count(a)); let b Cons(3, Rc::clone(a)); println!(count after creating b {}, Rc::strong_count(a)); { let c Cons(4, Rc::clone(a)); println!(count after creating c {}, Rc::strong_count(a)); } println!(count after c goes out of scope {}, Rc::strong_count(a)); }仓库中对应的实际运行输出 listing-15-19/output.txt 为$ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list) Finished dev profile [unoptimized debuginfo] target(s) in 0.45s Running target/debug/cons-list count after creating a 1 count after creating b 2 count after creating c 3 count after c goes out of scope 2输出验证了完整的行为闭环a创建后计数为1每次Rc::clone计数12、3c离开内部作用域后计数-1回到 2。计数下降是自动的Drop的实现注意下降不需要你调用任何函数。RcT实现了Droptrait当一个RcT值离开作用域时Drop会自动把引用计数减 1。这个例子看不到的隐藏部分是当b、a在main末尾依次离开作用域后计数归零RcList中的数据被完全清理。RcT的核心价值由此体现一个值可以有多个拥有者且只要还有任何一个拥有者存活计数就大于 0值就保证有效。为什么方法叫strong_count而不是countRcT除了强引用计数还有weak_count弱引用计数。weak_count的用途是防止引用循环reference cycles如果两个RcT互相持有对方强引用计数将永远无法归零造成内存泄漏。书中下一节 Preventing Reference Cycles UsingWeakT专门讲解了如何用WeakT打破这种循环。为什么RcT只允许不可变共享RcT通过不可变引用让你在程序的多个部分之间共享只读数据。它刻意不提供多路可变引用因为那会违反第 4 章讨论的借用规则之一对同一位置的多个可变借用会引发数据竞争与状态不一致。但现实需求往往需要多处共享 可变。解决方案正是本书接下来要讲的内部可变性interior mutability模式把RcT与RefCellT组合使用——RcT负责共享所有权RefCellT在运行时检查借用规则、允许安全地修改共享数据。这部分内容在仓库的 src/ch15-05-interior-mutability.md 中有完整实现与示例。小结与使用建议适用场景堆上数据需要被程序的多个部分只读共享且无法在编译期确定谁最后用完——用RcT两个 API 记牢Rc::new创建、Rc::clone递增计数廉价非深拷贝、Rc::strong_count观察计数自动回收计数归零由Drop自动完成无需手动减计数线程边界单线程用RcT多线程请改用原子计数版本的ArcT见 src/ch16-00-concurrency.md可变需求RcT只支持不可变共享若要修改共享数据需配合RefCellT使用内部可变性见 src/ch15-05-interior-mutability.md循环风险RcT强引用可能形成循环导致泄漏必要时用WeakT打断循环见 src/ch15-06-reference-cycles.md。想要亲手验证可进入仓库对应示例目录如listings/ch15-smart-pointers/listing-15-18/、listing-15-19/执行cargo run直观观察编译错误与引用计数的实时变化。赞分享教程文档【免费下载链接】bookThe Rust Programming Language项目地址https://gitcode.com/gh_mirrors/bo/book点击查看免费下载相关推荐Rust 引用计数智能指针 RcT 完全指南多所有权共享与引用计数原理The Rust Programming Language 实战解析Rust 引用计数智能指针 RcT 完全指南多所有权共享与引用计数原理The Rust Programming Language 实战解析 RcT 教程文档Rust Arc 原子引用计数详解跨线程安全共享所有权与实战Rust Arc 原子引用计数详解跨线程安全共享所有权与实战 Rust 的所有权模型保证了内存安全但也使得多个持有者共享同一份数据成为一个需要专门工具解文档教程Rust 关联关系Association实战用 Rc、Weak 与 RefCell 实现共享所有权与双向对象关联Rust 关联关系Association实战用 Rc 、 Weak 与 RefCell 实现共享所有权与双向对象关联 导读 在 Rust 中实现 OOP示例工程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表