Rust 学习笔记:Rc
Rust 学习笔记:Rc<T>
在某些情况下,单个值可能有多个所有者。例如,在图数据结构中,多个边可能指向同一个节点,并且该节点在概念上由指向它的所有边拥有。一个节点不应该被清理,除非它没有任何边指向它,因此没有所有者。
Rc<T> 显式地启用多重所有权,这是引用计数的缩写。Rc<T> 类型跟踪对一个值的引用次数,以确定该值是否仍在使用。
使用引用计数指针共享数据
尝试实现下图所示的数据结构,共有 a、b、c 三个列表,其中 b、c 共享 a。
我们先用 Box<T> 定义的 cons 列表试试:
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));
}
程序执行报错。Cons 变量拥有它们保存的数据,所以当我们创建 b 列表时,a 被移动到 b 中,而 b 拥有 a。然后,当我们试图在创建 c 时再次使用 a 时,我们不允许这样做,因为 a 已经被移动了。
我们可以将 Cons 的定义改为保存引用,但这样就必须指定生命周期形参。通过指定生命周期参数,我们将指定列表中的每个元素至少与整个列表一样长,但并不是每个场景都是如此。所以这不是一个好办法。
正确的做法是改变 List 的定义,使用 Rc<T> 代替 Box<T>。每个 Cons 变量现在都将保存一个值和一个 Rc<T>,该指针指向一个 List。
当我们创建 b 时,我们将克隆 a 所持有的 Rc<List>,而不是获取 a 的所有权,从而将引用的数量从 1 增加到 2,并让 a 和 b 共享数据的所有权。我们还将在创建 c 时再次克隆 a,将引用的数量从 2 增加到 3。每次调用 Rc::clone 时,对 Rc<List> 中的数据的引用计数将增加,并且除非对数据的引用为 0,否则数据不会被清理。
修改之前的代码:
use crate::List::{Cons, Nil};
use std::rc::Rc;
enum List {
Cons(i32, Rc<List>),
Nil,
}
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> 带入作用域,因为它不在 prelude 中。当我们创建 b 和 c 时,我们调用 Rc::clone 函数,并将一个 a 的不可变引用作为参数。
我们本可以调用 a.clone(),而不是 Rc::clone(&a),但是 Rust 的惯例是在这种情况下使用 Rc::clone,因为它只增加引用计数,而不是像 clone 一样做深拷贝。
通过使用 Rc::clone 进行引用计数,我们可以直观地区分深拷贝类型的克隆和增加引用计数的克隆。在寻找代码中的性能问题时,我们只需要考虑深度拷贝克隆,并且可以忽略对 Rc::clone 的调用。
直观感受引用计数
修改之前的代码:
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。
我们可以看到 a 中的 Rc<List> 的初始引用计数为 1,然后每次调用 Rc::clone,计数增加 1。当 c 超出作用域时,计数减少 1。
我们必须调用 Rc::clone 来增加引用计数,但我们不需要调用函数来减少引用计数。因为 Rc<T> 实现了 Drop trait,当值超出作用域时自动减少引用计数。
在这个例子中我们看不到的是,当 b 和 a 在 main 函数的末尾超出作用域时,计数为 0,并且 Rc<List> 被完全清除。
通过不可变引用,Rc<T> 允许在程序的多个部分之间共享仅用于读取的数据。假如 Rc<T> 允许有多个可变引用,同一位置的多个可变借用会导致数据竞争和不一致,这不被所有权系统允许。