Rc<T>,引用计数智能指针

在大多数情况下,所有权是清晰的:你确切地知道哪个变量拥有给定的值。然而,在某些情况下,单个值可能具有多个所有者。例如,在图数据结构中,多条边可能指向同一个节点,而该节点在概念上由所有指向它的边所拥有。除非节点没有任何边指向它,并且因此没有所有者,否则不应清理该节点。

你必须通过使用 Rust 类型 Rc<T> 显式启用多重所有权,它是引用计数的缩写。Rc<T> 类型跟踪对值的引用数量,以确定该值是否仍在被使用。如果对某个值的引用计数为零,则可以清理该值,而不会使任何引用失效。

Rc<T> 想象成客厅里的电视。当一个人进来观看电视时,他们会打开它。其他人可以进入房间并观看电视。当最后一个人离开房间时,他们会关闭电视,因为它不再被使用。如果有人在其他人还在观看电视时关闭电视,剩下的电视观众会一片哗然!

当我们想要在堆上分配一些数据供程序的多个部分读取,并且我们无法在编译时确定哪个部分将最后完成使用数据时,我们会使用 Rc<T> 类型。如果我们知道哪个部分将最后完成,我们可以只让该部分成为数据的所有者,并且在编译时强制执行的正常所有权规则将生效。

请注意,Rc<T> 仅用于单线程场景。当我们在第 16 章讨论并发时,我们将介绍如何在多线程程序中进行引用计数。

使用 Rc<T> 共享数据

让我们回到列表 15-5 中的 cons list 示例。回想一下,我们使用 Box<T> 定义了它。这一次,我们将创建两个列表,它们都共享第三个列表的所有权。从概念上讲,这看起来类似于图 15-3

Two lists that share ownership of a third list

图 15-3:两个列表 bc,共享第三个列表 a 的所有权

我们将创建列表 a,其中包含 5 和 10。然后我们将创建另外两个列表:b 以 3 开头,c 以 4 开头。bc 列表都将继续连接到包含 5 和 10 的第一个 a 列表。换句话说,这两个列表将共享包含 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 变体将持有一个值和一个指向 ListRc<T>。当我们创建 b 时,我们不会取得 a 的所有权,而是克隆 a 持有的 Rc<List>,从而将引用数量从一增加到二,并让 ab 共享 Rc<List> 中数据的所有权。当创建 c 时,我们也会克隆 a,将引用数量从二增加到三。每次我们调用 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:使用 Rc<T>List 定义

我们需要添加一个 use 语句将 Rc<T> 引入作用域,因为它不在 prelude 中。在 main 中,我们创建了持有 5 和 10 的列表,并将其存储在 a 中的新 Rc<List> 中。然后,当我们创建 bc 时,我们调用 Rc::clone 函数并将对 aRc<List> 的引用作为参数传递。

我们可以调用 a.clone() 而不是 Rc::clone(&a),但 Rust 的惯例是在这种情况下使用 Rc::cloneRc::clone 的实现不像大多数类型的 clone 实现那样对所有数据进行深拷贝。调用 Rc::clone 只会增加引用计数,这不会花费太多时间。数据的深拷贝可能会花费大量时间。通过使用 Rc::clone 进行引用计数,我们可以直观地区分深拷贝类型的克隆和增加引用计数的克隆类型。在代码中查找性能问题时,我们只需要考虑深拷贝克隆,而可以忽略对 Rc::clone 的调用。

克隆 Rc<T> 会增加引用计数

让我们更改列表 15-18 中的工作示例,以便我们可以看到引用计数随着我们创建和删除对 aRc<List> 的引用而变化。

在列表 15-19 中,我们将更改 main,使其在列表 c 周围有一个内部作用域;然后我们可以看到当 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;我们将在 “防止引用循环:将 Rc<T> 转换为 Weak<T> 中看到 weak_count 的用途部分。

这段代码打印出以下内容

$ 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 来增加引用计数一样:Drop trait 的实现会在 Rc<T> 值超出作用域时自动减少引用计数。

在这个例子中我们看不到的是,当 b 然后 amain 的末尾超出作用域时,计数变为 0,并且 Rc<List> 被完全清理。使用 Rc<T> 允许单个值拥有多个所有者,并且计数确保只要任何所有者仍然存在,该值就保持有效。

通过不可变引用,Rc<T> 允许你在程序的多个部分之间共享数据以进行只读操作。如果 Rc<T> 允许你拥有多个可变引用,你可能会违反第 4 章中讨论的借用规则之一:对同一位置的多个可变借用可能会导致数据竞争和不一致。但是能够修改数据非常有用!在下一节中,我们将讨论内部可变性模式和 RefCell<T> 类型,你可以将它们与 Rc<T> 结合使用,以处理这种不可变性限制。