前言
Rust的所有权规则(ownership rule)允许在给定的范围内一次只存在一个所有者。但是,在某些情况下,需要与多个变量共享类型。例如,在GUI库中,每个子小部件(child widget)都需要有一个对其父容器小部件(parent container widget)的引用,以便根据来自用户发起的的调整大小事件(resize event)对子小部件进行布局。虽然生命周期允许开发者通过将父节点存储为&'a parent来从子节点引用父节点,但它通常受到值的生命周期参数a的限制。一旦范围结束,引用也就无效了。在这种情况下,我们需要更灵活的方法,这就涉及使用引用计数类型(reference counting types)。这些智能指针类型能够提供程序中值的共享所有权。
引用计数类型能够做到在细粒度级别上进行垃圾收集。在这种方法中,智能指针类型允许对封装的值有多个引用。在内部,智能指针保存它给出的引用数量的计数,并使用一个引用计数器(此处为refcount)活动,这里是一个整数值。当引用封装的智能指针值的变量超出作用域时,refcount值递减。一旦对该对象的所有引用都消失了,并且refcount达到0,就会释放该值。这就是引用计数指针通常的工作方式。
Rust为我们提供了两种参考计数指针类型:
- Rc<T>:主要用于单线程环境( single threaded environments)
- Arc<T>用于多线程环境(multi-threaded environments)
我们会在这一章节里探索单线程情况,而后在第8章“并发性”中讨论下多线程的情况。
Rc<T>
当我们与Rc类型交互时,它内部会发生以下变化:
- 当通过调用Clone()获取一个新的Rc共享引用时,Rc会增加它的内部引用计数。Rc在内部为其引用计数使用单元格类型( Cell type )
- 当引用超出作用域时,将其减1
- 当所有共享引用超出作用域时,引用计数变为零。此时,对Rc的最后一个drop调用将执行它的重新分配
使用引用计数容器为实现提供了更大的灵活性:我们可以像分发新副本一样分发值的副本,而不必精确跟踪引用何时超出了作用域。当然,这并不意味着我们可以以可变方式为内部值添加别名。
Rc<T>主要通过两种方法来使用:
- 利用静态方法Rc::new创建一个新的引用计数容器
- 利用Clone方法增加强引用计数,并分发一个新的Rc<T>
Rc内部保留两种引用: strong (Rc<T>) 和weak (Weak<T>).。两者都记录每种类型的引用已分发的数量,但只有当强引用计数达到0时,这些值才会被回收。这样做的动机是,数据结构的实现可能需要多次指向相同的东西。例如,树的实现可能同时具有对子节点和父节点的引用,但是对每个引用都递增计数器数值是不正确的,并会导致引用循环。下面的图就说明了引用循环的情况:
如上面图表所见,我们有两个变量var1和var2,它们引用两个资源Obj1和Obj2。除此之外,Obj1也有一个对Obj2的引用Obj2也有一个对Obj1的引用。当var1和var2超出范围时,Obj1和Obj2的引用计数都为1。它们不会被释放,因为它们仍然相互关联。
可以使用弱引用(weak reference)来打破引用循环。另一个例子是,链表可以通过以下方式实现:通过对下一项和上一项的引用计数来维护链接。更好的方法是对一个方向使用强引用,对另一个方向使用弱引用。
让我们看看这是怎么做的。下面是一个可能最不实用但最适合学习数据结构的最小实现,即单链表:
// linked_list.rs
use std::rc::Rc;
#[derive(Debug)]
struct LinkedList<T> {
head: Option<Rc<Node<T>>>
}
#[derive(Debug)]
struct Node<T> {
next: Option<Rc<Node<T>>>,
data: T
}
impl<T> LinkedList<T> {
fn new() -> Self {
LinkedList { head: None }
}
fn append(&self, data: T) -> Self {
LinkedList {
head: Some(Rc::new(Node {
data: data,
next: self.head.clone()
}))
}
}
}
fn main() {
let list_of_nums = LinkedList::new().append(1).append(2);
println!("nums: {:?}", list_of_nums);
let list_of_strs = LinkedList::new().append("foo").append("bar");
println!("strs: {:?}", list_of_strs);
}
看一下,这个链表由两个结构体组成:LinkedList提供对列表第一个元素和列表的公共API的引用,Node包含实际的元素。请注意我们是如何使用Rc和在每个追加(append)上克隆下一个数据指针的。那么来看看在追加情况下发生了什么:
- LinkedList::new()给我们一个新的列表,head:None
- 我们向列表追加1。head作为数据的节点,现在是包含1,下一个是前一个的head:None
- 我们向列表追加2。head作为数据的节点,现在是包含2,下一个是前一个的head:1
debug的输出println!证实了这一点:
不难看出,这体现出这个结构的一种函数式形式;每个append的工作方式都是在head(头部)添加数据,这意味着我们不必使用引用,而实际的列表引用可以保持不变。如果我们想保持这个简单的结构,但仍然有一个双链表,那么我们实际上必须改变现有的结构。
结语
那么如何做相应的改变呢,姑且作为这一篇的作业,读者可以想一下,我们下篇再说。
主要参考和建议读者进一步阅读的文献
https://doc.rust-lang.org/book
深入浅出 Rust,2018,范长春
Rust编程之道,2019, 张汉东
The Complete Rust Programming Reference Guide,2019, Rahul Sharma,Vesa Kaihlavirta,Claus Matzinger
Hands-On Data Structures and Algorithms with Rust,2018,Claus Matzinger
Beginning Rust ,2018,Carlo Milanesi
Rust Cookbook,2017,Vigneshwer Dhinakaran