rc<T>
,参考计数智能指针
在大多数情况下,所有权是明确的:您确切地知道哪个变量拥有给定的值。但是,在某些情况下,单个值可能具有多个所有者。例如,在图形数据结构中,多条边可能指向同一个节点,并且该节点在概念上由指向它的所有边拥有。除非节点没有任何指向它的边缘,因此没有所有者,否则不应清理该节点。
您必须使用 Rust 类型显式启用多个所有权
Rc<T>
,这是参考计数的缩写。Rc<T>
类型跟踪对值的引用数,以确定该值是否仍在使用中。如果对某个值的引用为零,则可以清理该值,而不会使任何引用无效。
将 Rc<T>
想象成家庭活动室中的电视。当一个人进来看电视时,他们会打开电视。其他人可以进入房间看电视。当最后一个人离开房间时,他们会关掉电视,因为它不再被使用。如果有人在其他人仍在看电视时关掉电视,剩下的电视观众会一片哗然!
当我们想在堆上分配一些数据供程序的多个部分读取时,我们使用 Rc<T>
类型,并且我们无法在编译时确定哪一部分将最后使用数据。如果我们知道哪个部分最后完成,我们就可以将该部分设为数据的所有者,那么在编译时强制执行的正常所有权规则就会生效。
请注意,Rc<T>
仅用于单线程方案。当我们在第 16 章讨论并发时,我们将介绍如何在多线程程序中进行引用计数。
使用 Rc<T>
共享数据
让我们回到示例 15-5 中的 cons 列表示例。回想一下,我们使用 Box<T>
定义了它。这一次,我们将创建两个列表,这两个列表都共享第三个列表的所有权。从概念上讲,这类似于图 15-3:
图 15-3:两个列表 b
和 c
,共享第三个列表 a
的所有权
我们将创建包含 5 个和 10 个的列表 a
。然后我们将再创建两个列表:b
以 3 开头,c
以 4 开头。b
和 c
然后,列表将继续到第一个包含
5 和 10 的列表。换句话说,两个列表将共享包含 5 和 10 的第一个列表。
尝试使用带有 Box<T>
的 List
定义来实现此方案
不起作用,如示例 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
。然后,当我们在创建 c
时尝试再次使用 a
时,不允许这样做,因为 a
已被移动。
我们可以更改 Cons
的定义以保存引用,但随后我们必须指定生命周期参数。通过指定生命周期参数,我们将指定列表中的每个元素的寿命至少与整个列表一样长。示例 15-17 中的元素和列表是这种情况,但并非每个场景都是如此。
相反,我们将更改 List
的定义,以使用 Rc<T>
代替
Box<T>
,如示例 15-18 所示。每个 Cons
变体现在都将包含一个值和一个指向列表
的 Rc<T>
。当我们创建 b
时,我们将克隆 a
持有的 Rc<List>
,而不是获得 a
的所有权,从而将引用的数量从 1 增加到 2,并让 a
和 b
共享该 Rc<List>
中数据的所有权。我们还将在创建 c
时克隆 a
,将引用数量从 2 个增加到 3 个。每次调用 Rc::clone
时,对 Rc<List>
中数据的引用计数都会增加,除非对数据的引用为零,否则数据不会被清理。
文件名: 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>
引入范围,因为它不在序言中。在 main
中,我们创建包含 5 和 10 的列表,并将其存储在 a
中的新 Rc<List>
中。然后,当我们创建 b
和 c
时,我们调用
Rc::clone
函数,并将对 a 中 Rc<List>
的引用作为参数传递。
我们本可以调用 a.clone()
而不是 Rc::clone(&a),
但 Rust 的约定是在这种情况下使用 Rc::clone
。的实现
Rc::clone
不会像大多数类型的 clone
实现那样对所有数据进行深层复制。对 Rc::clone
的调用只会增加引用计数,这不会花费太多时间。数据的深层副本可能需要大量时间。通过使用 Rc::clone
进行引用计数,我们可以直观地
区分深拷贝类型的克隆和以下类型的克隆
增加引用计数。在
代码中,我们只需要考虑 deep-copy 克隆,并且可以忽略对
Rc::clone 的 Clone
中。
克隆 Rc<T>
会增加引用计数
让我们更改示例 15-18 中的工作示例,这样我们可以看到当我们在 a
中创建和删除对 Rc<List>
的引用时,引用计数会发生变化。
在示例 15-19 中,我们将更改 main
,使其在 list c
周围有一个内部作用域;然后我们可以看到当 C 超出范围时 reference
count 是如何变化的。
文件名: 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
我们可以看到 a
中的 Rc<List>
的初始引用计数为 1;然后,每次调用 clone
时,计数都会增加 1。当 c
超出范围时,计数将减少 1。我们不必像调用 Rc::clone
那样调用函数来减少引用计数:当 Rc<T>
值超出范围时,Drop
trait 的实现会自动减少引用计数。
在这个例子中,我们看不到的是,当 b
和 a
在 main
的末尾超出范围时,计数为 0,并且 Rc<List>
被完全清理。使用 Rc<T>
允许单个值具有多个所有者,并且计数可确保只要任何所有者仍然存在,该值就保持有效。
通过不可变引用,Rc<T>
允许您在程序的多个部分之间共享数据,仅用于读取。如果 Rc<T>
也允许你拥有多个可变引用,那么你可能违反了第 4 章中讨论的借用规则之一:对同一位置的多个可变借用会导致数据竞争和不一致。但是能够更改数据非常有用!在下一节中,我们将讨论内部可变性模式和 RefCell<T>
类型,您可以将其与 Rc<T>
结合使用,以处理此不可变性限制。