【Rust自学】15.5. Rc<T>:引用计数智能指针与共享所有权
喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
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
,分别是a
、b
和c
。其中b
和c
共享a
。其余信息如图:
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
的结构写出来 - 然后把
b
和c
的第一层写出来,嵌套的下一层直接写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
本身一样。借用检查器会阻止我们编译这样的代码:
let a = Cons(10, &Nil);
Nil
是一个零大小(zero-sized)的枚举变体,但是在表达式Cons(10, &Nil)
或&Nil
中,编译器会把它视作一个临时值,这个临时值通常只在当前语句(或更小的作用域)里生效,之后就被自动丢弃。
简单地来说,&Nil
是个临时变量,用完就被销毁,生命周期比enum
短。临时创建的Nil
的变体值会在a
取得其引用前就被丢弃。
正确的方法是使用Rc<T>
,用引用计数智能指针来让多个所有者共享同一块堆上的数据,并且在所有者都不用后自动释放内存:
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));
}
在声明b
和c
时,使用Rc::clone
并把a
的引用&a
作为参数传进去,这样b
和c
就不会获得a
的所有权,同时每使用一次Rc::clone
就会把智能指针内的引用计数加1。
创建a
时使用Rc::new
算第一次引用,此时计数器为1;在b
和c
中各使用了Rc::clone
一次,引用计数就会各加1,最终引用计数就是3。a
这个智能指针中的数据只有在引用计数为0时才会被清理掉。
其实在Rc<T>
上也有clone
方法(不是Clone
trait的上的clone
方法),其源码与Rc::clone
完全一样,所以在给b
和c
赋值时写a.clone()
也是可以的。但因为这么写可能会被误解为深拷贝(尤其是对新手来说),而实际它只是增加了引用计数,所以不推荐这么写,更多还是使用Rc::clone
。
接下来我们修改一下main
函数,打印一些帮助信息,看看当c
超出范围时引用计数如何变化:
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
会比a
和b
先走出作用域,所以在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
在此示例中我们看不到的是,当b
和a
在main
末尾超出范围时,计数为 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>
结合使用来处理此不变性限制。下一篇文章会讲到。