欢迎光临
我们一直在努力

Rust 语言特性:共享指针 Rc<T>

在日常编程中,我们经常会遇到“一份数据会被多个“所有者”共享的情形”,比如 Cache / Config / Session 中的数据基本都是此类情形。从所有权的视角来看:这些类型的数据并不具有唯一的、明确的所有者,而更倾向于是多个所有者共同拥有,当所有的持有者都不再使用时,再销毁它们。其实这就是 C++ 中的共享指针:shared_ptr,我们在《C++ 智能指针:示例、原理、适用场景全方位解读》一文中系统地介绍过它,有 C/C++ 背景的读者可以参考一下。本文,我们将介绍 Rust 中的共享指针:Rc<T> 引用计数。此外,真诚推荐本博客近期的两篇精华好文《你真得懂 Rust 借用检查吗?一文聊透 Borrow Checker 底层逻辑:权限模型》和《深入理解 Rust 生命周期(可能是全网最透彻的解读了)》

1. 使用 Rc<T> 共享数据

在此前的《Rust 语言特性:智能指针 Box<T> 》一文中,我们介绍过一种 cons list 数据结构,就是使用递归形式定义的 List,这里,我们继续用它来举例。首先,我们先构造一个 cons list 来出:

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)); // 此处 a 的所有权已转移到了 Box 实例中去了
// let c = Cons(4, Box::new(a));
}

得益于 Box 类型,让我们能构造出这种“递归形式”的数据结构。其中,程序的最后两行被注释掉了,因为它们会编译报错。我们来看一下最后两行的意图是什么。如文章开头所说,有时候,一份数据需要被多方所共有,假设我们现在想构建这样一个 cons list:

也就是说:我们先构建一个 cons list:a,然后,以 a 作为一部分,再分别构建出 b 和 c 两个 cons list,也就是被注释掉的两行所做的事情,但是,如果取消掉注释,代码会在最后一行(第 11 行)报错:Box::new(a) value used here after move,这个错误还是很合理的:在第 10 行时,a 的所有权已经在移动到 b 中去了(准确地说是 b 的第二个元素 Box 实例)。这是怎么发生的呢,其实关键是第 10 行的 Box::new(a),new 方法的签名是:fn new(x: T) -> Box<T> (参考「 官方文档 」),new 方法要求的是“传值”而不是“传引用”,所以,凡是通过 new 方法创建的 Box 类型,传给它的值的所有全权都会转移到 Box 中,这显然是刻意这样设计的,因为 Box 扮演智能指针的作用,必须要获得值的所有权。因此,当 11 行再次尝试将 a 赋值给第二个 Box 时,就报错了。

很显然,在这里,我们正试图让 a 变成一种可以被共享的数据,也就是能同时被 b 和 c 拥有。怎么办呢?方法就是:把 Box<T> 替换成 Rc<T> 就可以了,Rc 是 Reference Counted(引用计数)的意思,它是 Rust 中的共享指针,不会要求独占 a 的所有权!看修改后的版本:

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 实现的这一版中,除了使用 Rc<T> 替换 Box<T> 外,最与众不同的地方就是第 9、10 两行的 Rc::clone(&a),显然,这里肯定不是深度克隆,而是通过使用 Rc::clone 进行引用计数!

2. 对齐概念

了解了 Rc<T> 的基本结构后,为了保持以后的规范行文和沟通,我们得给 Rc<T> 在对堆上的值和在栈上的多个所有者约定一下规范的称谓。通常,我们把位于堆上的保存数据的实体称为:共享所有权的值或共享数据;把位于栈上的指针称为:共享所有权指针或 Rc 指针:

每个 Rc 指针本身存在栈上,它们都指向同一个堆数据,每多一个栈上的 Rc 指针,引用计数就 +1,这些栈上的 Rc 指针是堆上共享数据的共同所有者。

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

引用计数是共享指针的内部实现细节,关于它是如何工作的,我们在《C++ 智能指针:示例、原理、适用场景全方位解读》一文有过详细的解读,尽管它介绍的是 C++ 的共享指针,但内部实现的原理是一样的。为了对 Rc<T> 引用计数有一个更真实的了解,也是为验证我们的猜测,也就是使用 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));
}

程序输出:

count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

熟悉 C++ 智能指针的朋友应该能很容易理解 strong_count 是什么,因为实现原理都是一样的。这里我们先简单介绍一下,后面会结合 Weak<T> 给出更全面的介绍。对于 Rc<T> 这种共享指针来说,其内部必须要维护两个引用计数器,一个是强引用计数器,一个是弱引用计数器,针对 Rc 包裹的同一个值,每当通过 clone 创建了一个新的 Rc<T> 指针时,强引用计数器就会加 1,而当一个 Rc<T> 指针离开作用域被自动销毁时,强引用计数器就会减 1,当最后一个 Rc<T> 指针被销毁时,强引用计数器就会减为 0,此时会连带释放位于堆上的共享数据:

上图展示了 Rc 指针 / Weak 指针和强/弱引用计数器之间的关系。关于 Weak 指针和弱引用计数器,我们将会在后续的文章中专门介绍。

赞(0)
未经允许不得转载:171主机测评 » Rust 语言特性:共享指针 Rc<T>
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址