【Rust自学】15.5. Rc<T>:引用计数智能指针与共享所有权

server/2025/2/3 13:43:54/

喜欢的话别忘了点赞、收藏加关注哦,对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
请添加图片描述

15.5.1. 什么是Rc<T>

所有权在大部分情况下都是清晰的。对于一个给定的值,程序员可以准确地推断出哪个变量拥有它。

但是在某些场景中,单个值也可能同时被多个所有者持有,如下图:
请添加图片描述

在这个图数据结构中,其中的每个节点都有多条边指向它,所以这些节点从概念上讲就是同时属于所以指向它的边。而一个节点只要还有边指向它时就不应该被清理掉。这就是一种多重所有权。

为了支持多重所有权,Rust提供了Rc<T>类型,Rc是Reference counting(引用计数)的简写,这个类型会在实例的内部维护一个用于记录值的引用次数的计数器,从而判断这个值是否仍在使用。如果这个值的引用数量为0,那么这个值就可以被安全地清理掉了,而且不会触发引用实效的问题。

15.5.2. Rc<T>使用场景

当你希望将堆上的一些数据分享给程序的多个部分使用,但是在编译时又无法确定到底是程序的哪个部分最后使用完这些数据时,就可以使用Rc<T>

相反的,如果我们能在编译时确定程序的哪个部分会最后使用数据,那么只需要让这部分代码成为数据的所有者即可。这样依靠编译时的所有权规则就可以保证程序的正确性了。

需要注意的是,Rc<T>只能用于单线程场景,在以后的文章会研究如何在多线程中使用引用计数。

15.5.3. Rc<T>使用例

在使用前需要注意,Rc<T>不在预导入模块里,想要使用得先手动导入。

Rc下有这么一些基本的函数:

  • Rc::clone(&a)函数可以增加引用计数
  • Rc::strong_count(&a)可以获得引用计数,而且是强引用的计数
  • 既然有强引用,那就会有弱引用,也就是Rc::weak_count函数

用个例子来探究Rc<T>的实际应用:

一共有3个List,分别是abc。其中bc共享a。其余信息如图:
请添加图片描述

rust">enum List {  Cons(i32, Box<List>),  Nil,  
}  use List::{Cons, Nil};fn main() {// 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));  
}
  • 首先创建了一个链表List,其写法在 15.1. 使用Box<T>来指向堆内存上的数据 中就有详细解释,这里不在阐述
  • main函数中先把a的结构写出来
  • 然后把bc的第一层写出来,嵌套的下一层直接写a即可。

逻辑没有问题,运行一下试试:

error[E0382]: use of moved value: `a`--> src/main.rs:17:27|
10 |     let a = Cons(5,|         - move occurs because `a` has type `List`, which does not implement the `Copy` trait
...
15 |                  Box::new(a));|                           - value moved here
16 |     let c = Cons(4,
17 |                  Box::new(a));|                           ^ value used here after move

报错内容是使用了已移动的值。这是因为在写b时写道了a所以a的所有权就被移到b里了。

这该怎么改呢?

一种办法是修改List的定义,让Cons持有引用而不是所有权,并且要为它指定对应的生命周期参数,但这个生命周期参数会要求List中所有元素的存活时间至少要和List本身一样。借用检查器会阻止我们编译这样的代码:

rust">let a = Cons(10, &Nil);

Nil是一个零大小(zero-sized)的枚举变体,但是在表达式Cons(10, &Nil)&Nil中,编译器会把它视作一个临时值,这个临时值通常只在当前语句(或更小的作用域)里生效,之后就被自动丢弃。

简单地来说,&Nil是个临时变量,用完就被销毁,生命周期比enum短。临时创建的Nil的变体值会在a取得其引用前就被丢弃。

正确的方法是使用Rc<T>,用引用计数智能指针来让多个所有者共享同一块堆上的数据,并且在所有者都不用后自动释放内存:

rust">enum List {  Cons(i32, Rc<List>),  Nil,  
}  use List::{Cons, Nil};  
use std::rc::Rc;  fn main() {  // 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));  
}

在声明bc时,使用Rc::clone并把a的引用&a作为参数传进去,这样bc就不会获得a的所有权,同时每使用一次Rc::clone就会把智能指针内的引用计数加1。

创建a时使用Rc::new算第一次引用,此时计数器为1;在bc中各使用了Rc::clone一次,引用计数就会各加1,最终引用计数就是3。a这个智能指针中的数据只有在引用计数为0时才会被清理掉。

其实在Rc<T>上也有clone方法(不是Clone trait的上的clone方法),其源码与Rc::clone完全一样,所以在给bc赋值时写a.clone()也是可以的。但因为这么写可能会被误解为深拷贝(尤其是对新手来说),而实际它只是增加了引用计数,所以不推荐这么写,更多还是使用Rc::clone

接下来我们修改一下main函数,打印一些帮助信息,看看当c超出范围时引用计数如何变化:

rust">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));
}

这里c会比ab先走出作用域,所以在c走出作用域后引用计数会减1。

输出:

count after creating a = 1 
count after creating b = 2 
count after creating c = 3 
count after c goes out of scope = 2

在此示例中我们看不到的是,当bamain末尾超出范围时,计数为 0,并且Rc<List>被完全清理。

因为Rc<T>实现了Drop trait,所以当Rc<T>离开作用域时引用计数器会自动减1。使用Rc<T>允许单个值拥有多个所有者,并且计数可确保只要任何所有者仍然存在,该值就保持有效。

15.5.4. Rc<T>总结

Rc<T>通过不可变引用,使程序员可以在程序的不同部分之间共享只读的数据。

这里再次强调,Rc<T>引用是不可变的,如果Rc<T>允许程序员持有多个可变引用的话就会违反借用规则(详见 4.4. 引用与借用)——多个指向同一区域的可变引用会导致数据竞争以及数据的不一致。

而在实际开发中肯定会遇到需要数据可变的情况,针对它Rust提供了内部可变性模式和RefCell<T>,程序员可以将其与Rc<T>结合使用来处理此不变性限制。下一篇文章会讲到。


http://www.ppmy.cn/server/164625.html

相关文章

[SAP ABAP] 在ABAP Debugger调试器中设置断点

在命令框输入/H&#xff0c;点击回车以后&#xff0c;调试被激活&#xff0c;点击触发任意事件进入ABAP Debugger调试器界面 点击按钮&#xff0c;可以在Debugger调试器中新增临时断点 我们可以从ABAP命令、方法、功能、表单、异常、消息、源代码等多个维度在Debugger调试器中设…

线程的状态转换和调度

新建状态New&#xff1a;新创建了一个线程对象 可运行状态Runnable&#xff1a;线程对象创建后&#xff0c;其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中&#xff0c;变得可运行&#xff0c;等待获取CPU的使用权。 运行状态Running&#xff1a;可运行…

【JavaEE】Spring(7):统一功能处理

一、拦截器 拦截器的使用步骤&#xff1a; 定义拦截器注册配置拦截器 1. 定义拦截器 Slf4j Component public class LoginInterceptor implements HandlerInterceptor {Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Objec…

基于微信小程序的辅助教学系统的设计与实现

标题:基于微信小程序的辅助教学系统的设计与实现 内容:1.摘要 摘要&#xff1a;随着移动互联网的普及和微信小程序的兴起&#xff0c;基于微信小程序的辅助教学系统成为了教育领域的一个新的研究热点。本文旨在设计和实现一个基于微信小程序的辅助教学系统&#xff0c;以提高教…

CTFSHOW-WEB入门-命令执行54-70

题目&#xff1a;web 54 题目&#xff1a;解题思路&#xff1a;分析题目可以知道&#xff0c;题目过滤了&#xff1a; 分号&#xff1b;空格 数字 制表符 百分号% 反引号 大于号> 小于号< 中间若干个命令过滤方式&#xff0c;以cat为例&#xff1a; 这些字符 ‘c’、‘a…

【Redis】set 和 zset 类型的介绍和常用命令

1. set 1.1 介绍 set 类型和 list 不同的是&#xff0c;存储的元素是无序的&#xff0c;并且元素不允许重复&#xff0c;Redis 除了支持集合内的增删查改操作&#xff0c;还支持多个集合取交集&#xff0c;并集&#xff0c;差集 1.2 常用命令 命令 介绍 时间复杂度 sadd …

RTOS面试合集

目录 啥是RTOS 进程的状态 RTOS的调度 什么是时间片轮转调度&#xff1f; 如何实现基于优先级的抢占调度&#xff1f; 任务调度中的“时间窗”概念是什么&#xff1f; 任务和线程 如何在RTOS中创建和删除任务&#xff1f; 任务间如何共享数据&#xff1f; 如何管理任务…

01.双Android容器解决方案

目录 写在前面 一&#xff0c;容器 1.1 容器的原理 1.1.1 Namespace 1.1.2 Cgroups&#xff08;Control Groups&#xff09; 1.1.3 联合文件系统&#xff08;Union File System&#xff09; 1.2 容器的应用 1.2.1 微服务架构 1.2.2 持续集成和持续部署&#xff08;CI/…