Rc<T>
,引用计数的智能指针
在大多数情况下,所有权是明确的:你可以确切地知道哪个变量拥有某个值。然而,有些情况下,一个值可能会有多个所有者。例如,在图数据结构中,多个边可能指向同一个节点,而这个节点在概念上是由所有指向它的边共同拥有的。除非没有任何边指向该节点,否则该节点不应该被清理。
你必须通过使用 Rust 类型 Rc<T>
来显式地启用多重所有权,Rc<T>
是 reference counting(引用计数)的缩写。Rc<T>
类型会跟踪一个值的引用数量,以确定该值是否仍在被使用。如果某个值的引用数量为零,那么该值可以被清理,而不会导致任何引用失效。
你可以把 Rc<T>
想象成家庭房间里的电视。当一个人进入房间看电视时,他们会打开电视。其他人也可以进入房间并观看电视。当最后一个人离开房间时,他们会关闭电视,因为电视不再被使用。如果有人在其他人还在看电视时关闭电视,那么剩下的观众会感到非常不满!
当我们希望在堆上分配一些数据,以便程序的多个部分可以读取,并且我们无法在编译时确定哪个部分会最后使用这些数据时,我们会使用 Rc<T>
类型。如果我们知道哪个部分会最后使用数据,我们可以直接让该部分成为数据的所有者,并在编译时强制执行正常的所有权规则。
需要注意的是,Rc<T>
仅适用于单线程场景。当我们在第 16 章讨论并发时,我们会介绍如何在多线程程序中进行引用计数。
使用 Rc<T>
共享数据
让我们回到 Listing 15-5 中的 cons 列表示例。回想一下,我们使用 Box<T>
定义了它。这次,我们将创建两个列表,它们共同拥有第三个列表的所有权。从概念上讲,这类似于图 15-3。
图 15-3:两个列表 b
和 c
,共享第三个列表 a
的所有权
我们将创建一个包含 5
和 10
的列表 a
。然后我们再创建两个列表:b
以 3
开头,c
以 4
开头。b
和 c
列表随后都会继续到包含 5
和 10
的第一个列表 a
。换句话说,这两个列表将共享包含 5
和 10
的第一个列表。
尝试使用我们定义的 List
和 Box<T>
来实现这个场景是行不通的,如 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));
}
当我们编译这段代码时,会得到以下错误:
$ 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
的定义,使其持有引用而不是数据,但这样我们就必须指定生命周期参数。通过指定生命周期参数,我们就是在指定列表中的每个元素至少要与整个列表一样长。这在 Listing 15-17 中的元素和列表中是成立的,但并非在所有场景中都成立。
相反,我们将修改 List
的定义,使用 Rc<T>
代替 Box<T>
,如 Listing 15-18 所示。每个 Cons
变体现在将持有一个值和一个指向 List
的 Rc<T>
。当我们创建 b
时,不再获取 a
的所有权,而是克隆 a
所持有的 Rc<List>
,从而将引用计数从 1 增加到 2,并让 a
和 b
共享该 Rc<List>
中的数据的所有权。在创建 c
时,我们也会克隆 a
,将引用计数从 2 增加到 3。每次我们调用 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)); }
我们需要添加一个 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
进行引用计数,我们可以在视觉上区分深拷贝类型的克隆和增加引用计数的克隆。在代码中寻找性能问题时,我们只需要考虑深拷贝类型的克隆,而可以忽略对 Rc::clone
的调用。
克隆 Rc<T>
会增加引用计数
让我们修改 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; 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 章讨论的借用规则之一:对同一位置的多个可变借用可能导致数据竞争和不一致。但能够改变数据是非常有用的!在下一节中,我们将讨论内部可变性模式以及你可以与 Rc<T>
结合使用的 RefCell<T>
类型,以应对这种不可变性限制。