携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第04天,点击查看活动详情
在前面的课程中,我们一起学习了 Rust 中的所有权原则以及如何在不取得所有权的前提下访问变量拥有的值。引用是 Rust 中使用最广泛的指针类型,它本身比较简单,出了指向某个值外并没有其他的功能,且不会造成性能上的额外损耗。除了引用外,指针类型还有其他的成员,例如引用计数 Rc。它们与引用相比,具有更复杂的数据结构,例如元数据(当前长度、最大可用长度等)。而且,引用并不具备值的所有权,而智能指针可以拥有它们指向的数据。今天的课程中,我们将重点学习几个最具代表性的智能指针:
Rc<T>,引用计数类型,允许多所有权存在Cell<T>和RefCell<T>,允许内部可变引用Box<T>,可以将值分配到堆上
01-Rc
Rc 为值 T 建立引用计数器,可以通过 Rc::clone 创建多个拥有所有权的智能指针,它们共同指向同一块堆内存。调用 Rc::clone 时只会浅拷贝栈上的智能指针,并将计数器加一。当栈上的智能指针离开其作用域时,drop 也只会回收栈上的智能指针并将计数器减一。直到计数器值为0时,才真正清除值 T 对应的内存。
我们可以看下 Rc::clone 的源码:
fn clone(&self) -> Rc<T> {
unsafe {
self.inner().inc_strong(); // 计数器递增
Self::from_inner(self.ptr) // 拷贝指针
}
}
注:Rc::clone 并非是深拷贝,仅仅复制了智能指针,并没有克隆底层数据,所以这种复制的效率是比较高的。
课程中有个非常形象的示意图,可以帮助我们理解 Rc 结构:
Rc 这样数据结构有什么应用场景呢?根据我们学习的所有权原则、借用语义,一个值只能有一个所有者,这在大多数场景中都没问题。但是考虑如下场景:
- 在图结构中,一个节点可能同时属于多条边,只有当它不属于任何边时,才能够被释放;
- 多线程环境下,多个线程可能会同时修改某个值,是无法同时获取该数据的可变引用的。
所以,Rust 引入了 Rc 和 Arc,允许一个数据资源在同一时刻可以有多个所有者。Rc 并不能在多线程之间传递,主要是其未实现 Send trait。在线程间传递时,需要用到 Rc 的线程安全版本 Arc,这里的 A 指的是 Atomic。
Rc 中持有的是数据的不可变引用,所以引用计数器创建后,其指向的值并不能改变。如果我们要改变指向的值,需要用到 Cell 或 RefCell。
02-Cell / RefCell
Cell 或 RefCell 在功能上没有区别,唯一的区别在于 Cell 适用的类型实现了 Copy trait。在实际的应用中,RefCell 使用的场景更广泛,主要用来解决可变引用、不可变引用共存导致的问题。
RefCell 涉及到了一个称为内不可变性的概念。
外部可变性:当使用 mut 或 &mut 声明的可变值或可变引用时,编译器会通过静态检查保证只有可变的值或可变的引用,才能修改值内部的数据。 内部可变性:希望能够绕开编译器的静态检查,即编译时值是可读的,但在运行时可以获得它的可变引用,从而修改内部的值。
Rust 官网文档中给出了 RefCell 可变和不可变借用的实例:
use std::cell::RefCell;
let c = RefCell::new(5);
let m = c.borrow();
let b = c.borrow_mut(); // this causes a panic
从例子中可以看出,RefCell 并没有违背同一个作用域中可变引用与只读引用不可共存的约束,只是将检查过程从编译时延迟到了运行时。《Rust语言圣经》中列出了 Rust 所有权规则、借用规则与智能指针区别的对比:
Rc 和 RefCell 通常会配合使用,前者可以实现一个数据有多个所有者,而后者可以实现数据的可变性。例如,课程中可变 DAG 实现中节点定义:
struct Node {
id: usize,
// 使用 Rc<RefCell<T>> 让节点可以被修改
downstream: Option<Rc<RefCell<Node>>>,
}
RefCell 同样也是非线程安全地,如果要在多线程环境下使用内部可变性,需要用 Mutex 和 RwLock。
03-Box
Box 也是一个非常常用的智能指针,它的作用就是在堆上分配空间存储值,然后在栈上保留一个智能指针指向堆中的数据。它的使用场景有:
- 特意地将数据存储在堆上。
- 数据较大,不希望在所有权转移时进行数据拷贝,例如[u32, 10000],Box::new([u32, 10000])可以在所有权转移时只拷贝栈上的智能指针。
- 类型的大小在编译期无法确定,但是我们又需要固定大小的类型时。
- 特征对象,用于说明对象实现了一个特征,而不是某个特定的类型。
Box 中有一个特殊关联函数 Box::leak,它可以消费掉 Box 并强制使目标值从堆上泄露出去。Box::leak 可以创建不受栈内存控制的堆内存,生命周期可以大到和整个进程的生命周期一致(静态生命周期),从而绕过编译时的所有权检查。
pub fn new(value: T) -> Rc<T> {
unsafe {
Self::from_inner(
Box::leak(box RcBox { strong: Cell::new(1), weak: Cell::new(1), value }).into(),
)
}
}
今天的课程链接:《09|所有权:一个值可以有多个所有者么?》
历史文章推荐