Rc<T>
、引用计数智能指针
在大多数情况下,所有权是明确的:您确切地知道哪个变量 拥有给定的值。但是,在某些情况下,单个值可能具有 多个所有者。例如,在图形数据结构中,多个边可能会 指向同一节点,并且该节点在概念上由所有边拥有 指向它。除非节点没有任何 edges 指向它,因此没有所有者。
您必须使用 Rust 类型显式启用多个所有权Rc<T>
,它是 reference counting 的缩写。这Rc<T>
类型
跟踪对值的引用数以确定
该值仍在使用中。如果对某个值的引用为零,则值
可以清理而不会使任何引用无效。
想象Rc<T>
作为家庭活动室的电视。当一个人进来看电视时,
他们打开了它。其他人可以进入房间看电视。当最后一个
人们离开房间,他们关掉了电视,因为它不再被使用。
如果有人在其他人仍在观看电视时关闭电视,就会有
其余电视观众的哗然!
我们使用Rc<T>
type 时,我们想在堆上为
要读取程序的多个部分,并且我们在编译时无法确定
哪个部分将最后完成对数据的使用。如果我们知道哪个部分会完成
最后,我们可以将这部分设为数据的所有者,而 Normal 所有权
在编译时强制执行的规则将生效。
请注意,Rc<T>
仅适用于单线程方案。当我们讨论
并发性,我们将介绍如何在
多线程程序。
用Rc<T>
共享数据
让我们回到示例 15-5 中的 cons 列表示例。回想一下,我们定义了
它使用Box<T>
.这一次,我们将创建两个共享所有权的列表
第三个列表。从概念上讲,这类似于图 15-3:
图 15-3:两个列表b
和c
、共享 的所有权
第三个列表a
我们将创建 lista
,其中包含 5,然后是 10。然后我们再制作两个
列表:b
以 3 开头,c
从 4 开始。双b
和c
然后,列表将继续执行第一个a
包含 5 和 10 的列表。在其他
words,则两个列表将共享包含 5 和 10 的第一个列表。
尝试使用我们的List
跟Box<T>
不起作用,如示例 15-17 所示:
文件名: src/main.rs
enum List {
Cons(i32, Box<List>),
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));
}
示例 15-17:证明我们不允许拥有
两个列表Box<T>
试图分享第三个列表的所有权
当我们编译此代码时,我们收到此错误:
$ 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
For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` (bin "cons-list") due to 1 previous error
这Cons
变体拥有它们持有的数据,因此当我们创建b
列表a
已移至b
和b
拥有a
.然后,当我们尝试使用a
again when
创建c
,我们不允许这样做,因为a
已移动。
我们可以更改Cons
来保留引用,但随后
我们必须指定生命周期参数。通过指定生命周期
参数,我们将指定列表中的每个元素都将位于
至少与整个列表一样长。元素和列表就是这种情况
在示例 15-17 中,但并非在每个场景中。
相反,我们将更改List
使用Rc<T>
代替Box<T>
,如示例 15-18 所示。每Cons
variant 现在将保存一个值
以及一个Rc<T>
指向List
.当我们创建b
,而不是采用
所有权a
,我们将克隆Rc<List>
那a
是持有,因此
将引用的数量从 1 增加到 2,然后让a
和b
共享该数据Rc<List>
.我们还将克隆a
什么时候
创建c
,将引用数量从 2 个增加到 3 个。每次
我们调用Rc::clone
,则对Rc<List>
将
increase,除非对
它。
文件名: src/main.rs
enum List { Cons(i32, Rc<List>), 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)); }
示例 15-18: 的定义List
使用Rc<T>
我们需要添加一个use
要带来的声明Rc<T>
into 范围,因为它不是
在序曲中。在main
,我们创建包含 5 和 10 的列表并将其存储在
一个新的Rc<List>
在a
.然后,当我们创建b
和c
,我们调用Rc::clone
函数,并将引用传递给Rc<List>
在a
作为
论点。
我们本可以调用a.clone()
而不是Rc::clone(&a)
,但 Rust 的
约定是使用Rc::clone
在这种情况下。的实现Rc::clone
不会像大多数类型那样对所有数据进行深层复制”
的实现clone
做。对Rc::clone
只会递增
reference count 的 Count,这不会花费太多时间。数据的深层副本可以采用
很多时间。通过使用Rc::clone
对于参考计数,我们可以直观地
区分深拷贝类型的克隆和以下类型的克隆
增加引用计数。在
代码中,我们只需要考虑 deep-copy 克隆,并且可以忽略对Rc::clone
.
克隆Rc<T>
增加引用计数
让我们更改示例 15-18 中的工作示例,以便我们可以看到参考
计数会发生变化,因为我们创建和删除对Rc<List>
在a
.
在示例 15-19 中,我们将更改main
所以它有一个内部作用域 around listc
;
那么我们可以看到引用计数是如何变化的:c
超出范围。
文件名: src/main.rs
enum List { Cons(i32, Rc<List>), 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)); }
示例 15-19:打印引用计数
在程序中引用计数发生变化的每个点,我们都会打印
引用计数,我们通过调用Rc::strong_count
功能。这
函数被命名为strong_count
而不是count
因为Rc<T>
类型
还有一个weak_count
;我们拭目以待weak_count
用于“防止参考循环:转动Rc<T>
转换为Weak<T>
”部分。
此代码打印以下内容:
$ 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
我们可以看到,Rc<List>
在a
初始引用计数为 1;然后
每次我们调用clone
,则计数增加 1。什么时候c
超出范围,
计数减少 1。我们不必调用函数来减少
引用计数,就像我们必须调用Rc::clone
增加引用
count:的Drop
trait 减少引用计数
当Rc<T>
value 超出范围。
在这个例子中,我们看不到的是,当b
然后a
超出范围
在main
,则计数为 0,并且Rc<List>
已清理
完全。用Rc<T>
允许单个值具有多个所有者,并且
count 可确保只要任何所有者
仍然存在。
通过不可变引用,Rc<T>
允许您在多个
程序的部件仅用于读取。如果Rc<T>
允许您拥有多个
可变引用,则可能会违反所讨论的借用规则之一
在第 4 章中:对同一位置的多个可变借用会导致数据竞争
和不一致。但是能够更改数据非常有用!在下一个
部分中,我们将讨论内部可变性模式和RefCell<T>
type 中,您可以将其与Rc<T>
使用这个
不可变性限制。
本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准