Rc<T>:引用计数智能指针
在大多数情况下,所有权是明确的:你确切地知道哪个变量拥有某个给定的值。然而,有时一个值可能有多个所有者。例如,在图数据结构中,多条边可能指向同一个节点,而该节点在概念上由所有指向它的边共同拥有。一个节点不应被清理,除非没有任何边指向它,也就是没有了所有者。
你必须通过使用 Rust 类型 Rc<T> 来显式启用多重所有权,它是引用计数(reference counting)的缩写。Rc<T> 类型会跟踪对一个值的引用数量,以确定该值是否仍在使用中。如果对某个值的引用数为零,该值就可以被清理,而不会出现任何引用变为无效的情况。
把 Rc<T> 想象成家庭活动室里的一台电视。当一个人进来看电视时,他会打开电视。其他人可以进来并一起看电视。当最后一个人离开房间时,他会关掉电视,因为电视不再被使用了。如果有人在其他人还在看电视的时候关掉电视,剩下的观看者肯定会闹起来!
当我们想把一些数据分配在堆上,让程序的多个部分读取,并且我们在编译时无法确定哪个部分会最后一个用完这些数据时,就需要使用 Rc<T> 类型。如果我们知道哪个部分会最后一个用完,我们就可以让那个部分成为数据的所有者,编译时强制执行的常规所有权规则就会生效。
注意,Rc<T> 只能用于单线程场景。当我们在第 16 章讨论并发时,我们将介绍如何在多线程程序中进行引用计数。
共享数据
让我们回到 Listing 15-5 中的 cons list 示例。回想一下,我们使用 Box<T> 定义了它。这次,我们将创建两个列表,它们都共享对第三个列表的所有权。从概念上讲,这看起来类似于图 15-3。
图 15-3:两个列表 b 和 c,共享对第三个列表 a 的所有权
我们将创建包含 5 然后 10 的列表 a。然后,我们再创建两个列表:以 3 开头的 b 和以 4 开头的 c。接着,b 和 c 列表都会继续连接到第一个包含 5 和 10 的 a 列表。换句话说,两个列表都将共享包含 5 和 10 的第一个列表。
尝试使用我们用 Box<T> 定义的 List 来实现这个场景是行不通的,如 Listing 15-17 所示。
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));
}
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
|
note: if `List` implemented `Clone`, you could clone the value
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^ consider implementing `Clone` for this type
...
10 | let b = Cons(3, Box::new(a));
| - you could clone this value
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 的定义,让它持有引用而非值,但这样我们就必须指定生命周期参数。通过指定生命周期参数,我们就是在指定列表中的每个元素都至少会与整个列表活得一样久。对于 Listing 15-17 中的元素和列表来说确实如此,但并非在每种情况下都如此。
相反,我们将改变 List 的定义,用 Rc<T> 代替 Box<T>,如 Listing 15-18 所示。每个 Cons 变体现在将持有一个值和一个指向 List 的 Rc<T>。当我们创建 b 时,我们不会取得 a 的所有权,而是会克隆 a 持有的 Rc<List>,从而将引用数量从一个增加到两个,并让 a 和 b 共享那个 Rc<List> 中数据的所有权。在创建 c 时我们也会克隆 a,将引用数量从两个增加到三个。每次我们调用 Rc::clone,Rc<List> 内部数据的引用计数都会增加,并且数据在引用数为零之前不会被清理。
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));
}
Rc<T> 的 List 定义我们需要添加一个 use 语句来将 Rc<T> 引入作用域,因为它不在 prelude 中。在 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 进行引用计数,我们可以在视觉上区分深拷贝类型的克隆和增加引用计数的克隆。在查找代码中的性能问题时,我们只需考虑深拷贝克隆,而可以忽略对 Rc::clone 的调用。
通过克隆增加引用计数
让我们修改 Listing 15-18 中可工作的示例,以便我们能看到在创建和丢弃对 a 中 Rc<List> 的引用时引用计数的变化。
在 Listing 15-19 中,我们将修改 main,使其在列表 c 周围有一个内部作用域;这样,我们就能看到 c 离开作用域时引用计数的变化。
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
// --snip--
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));
}
在程序中引用计数发生变化的每个点,我们都会打印引用计数,这通过调用 Rc::strong_count 函数获得。这个函数之所以命名为 strong_count 而不是 count,是因为 Rc<T> 类型还有一个 weak_count;我们将在「使用 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 然后 a 在 main 结束时离开作用域,计数变为 0,Rc<List> 被完全清理。使用 Rc<T> 允许单个值拥有多个所有者,计数确保只要任何所有者仍然存在,该值就保持有效。
通过不可变引用,Rc<T> 允许你在程序的多个部分之间共享数据,仅供读取。如果 Rc<T> 也允许你拥有多个可变引用,你可能会违反第 4 章中讨论的借用规则之一:对同一位置的多个可变借用可能导致数据竞争和不一致。但能够修改数据是非常有用的!在下一节中,我们将讨论内部可变性模式以及 RefCell<T> 类型,你可以将它与 Rc<T> 结合使用来应对这种不可变性的限制。