写点什么

Rust 从 0 到 1- 智能指针 -Rc<T>

用户头像
关注
发布于: 28 分钟前
Rust从0到1-智能指针-Rc<T>

大部分情况下所有权是非常明晰的:我们可以清楚的知道哪个变量拥有数据的所有权。但是,也存在拥有多个所有者的情况。譬如,在图数据结构中,多个边可能指向相同的结点,我们在某些场景下可以理解为这个结点被所有指向它的边所拥有,直到没有任何边指向它的时候才可以被清理。

为了可以使数据具有多所有权,Rust 提供了 Rc<T> 类型,即引用计数(reference counting)的缩写。Rc<T> 会记录一个数据引用的数量,如果某个数据的引用数为 0,即不存在任何有效引用了,那么就可以被清理掉了。

我们可以把 Rc<T> 想象为房间中的电视。当第一个人进来看电视时,他把电视打开;后面其他人也可以进来看电视,而当最后一个人离开房间时,因为没人在看电视了,他把电视关掉。如果有人在其他人还在看的时候就关掉了电视,其他人肯定会有意见!

Rc<T> 用于当我们希望存储在堆上的数据可以在程序中的多处进行读取,但是无法在编译时确定谁会最后结束使用它的场景。如果可以确切知道谁是最后一个使用者的话,其就可以做为数据的所有者,那么所有权规则就可以正常发生作用了。

注意 Rc<T> 只能用于单线程场景;后面介绍并发的时候会讨论如何在多线程场景下进行引用计数。

使用 Rc<T> 共享数据

让我们回到前面使用 Box<T> 实现 cons list 的例子。假设,我们希望创建两个列表,他们共享另外一个列表的所有权,看起来如下图所示(来自官网):


列表 b 从 3 开始,列表 c 从 4 开始。它们共享包含 5 和 10 的列表 a。如果我们尝试使用 Box<T> 去实现,将无法编译通过,参考下面的例子:

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` trait10 |     let b = Cons(3, Box::new(a));   |                              - value moved here11 |     let c = Cons(4, Box::new(a));   |                              ^ value used here after move
error: aborting due to previous error
For more information about this error, try `rustc --explain E0382`.error: could not compile `cons-list`
To learn more, run the command again with --verbose.
复制代码

Cons 会获取数据的所有权,所以当创建列表 b 时,a 的所有权被转移给了 b 。因此,当我们创建 c 时,无法再获得 a 的所有权,因为其所有权已经转移了。

我们可以改变 Cons 定义中的类型为引用,但是这样的话我们必须指定生命周期参数。通过生命周期参数,指定列表中的每一个元素都至少需要与整个列表存在的一样长。借用检查器不会让 let a = Cons(10, &Nil);  编译通过,因为 Nil 在 a 获取其引用之前就已经被丢弃了。在这种情况下,我们可以使用 Rc<T> 代替 Box<T>,参考下面的例子:

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));}
复制代码

上面的例子中,每一个 Cons 类型的数据都包含一个 i32 类型的值和一个包含 List 类型值的 Rc 类型值。当创建列表 b 时,我们会克隆变量 a ,这时引用计数会加 1(从 1 变为 2)并允许 a 和 b 共享 Rc 中数据的所有权。创建列表 c 时又克隆了变量 a,这时引用计数又加 1(从 2 变为 3)。即,每次调用 Rc::clone ,Rc<List> 中数据的引用计数都会增加,并且在引用计数变为 0 之前数据都不会被清理。

我们也可以使用 a.clone() 进行克隆操作,但是 Rust 中的习惯用法是 Rc::clone。Rc::clone 并不像大多数类型的 clone 方法那样实现了对数据的深度拷贝。Rc::clone 只会增加引用计数,这不会消耗多少时间,而深度拷贝可能需要花费比较长的时间。使用 Rc::clone 进行引用计数,可以将深度拷贝的行为和增加引用计数的行为明显的区分出来。这在我们排查代码中的性能问题时,可以将引用计数类的克隆排除,而只需关注深度拷贝类的克隆。

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

让我们对前面的例子做一些修改,以便于观察引用计数的变化。并将列表 c 置于内部作用域中,这样就可以观察到当列表 c 离开作用域时引用计数的变化。参考下面的例子:

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_count 的用途。尝试运行上面的例子,我们会得到类似下面的结果:

$ cargo run   Compiling cons-list v0.1.0 (file:///projects/cons-list)    Finished dev [unoptimized + debuginfo] target(s) in 0.45s     Running `target/debug/cons-list`count after creating a = 1count after creating b = 2count after creating c = 3count after c goes out of scope = 2
复制代码

从结果中我们可以看到 Rc<List> 的初始引用计数为 1,后面每次调用 Rc::clone,计数都会增加 1。当 c 离开作用域时,计数减 1。我们需要主动调用一个函数来减少计数;Rc<T> 实现的 Drop trait 在其实例离开作用域时自动减少引用计数。在结果中我们并未展示出来的是:在 main 函数的结尾,当 b 和 a 相继离开作用域时,计数就会变为 0,这时 Rc 就会被清理掉。

通过不可变引用, Rc<T> 允许在程序中多处只读地共享数据。如果 Rc<T> 也允许可变引用,就可能会违反借用规则:同一处的多个可变引用可能造成数据竞争和不一致。但不可避免的在有些场景下我们需要修改数据!下面我们将讨论在这种场景下要如何处理。

发布于: 28 分钟前阅读数: 2
用户头像

关注

公众号"山 顽石"欢迎大家关注;) 2021.03.23 加入

IT老兵,关注Rust、Java、前端、架构、大数据、AI、算法等;平时喜欢美食、旅游、宠物、健身、篮球、乒乓球等。希望能和大家交流分享。

评论

发布
暂无评论
Rust从0到1-智能指针-Rc<T>