今天是大年初一,首先给大家拜个年!
祝大家新春快乐,最重要的是身体健康!
我们之前介绍的单一所有权,其实已经能满足我们使用内存的大部分场景。在编译时就能完成静态检查,不会影响运行时的效率。
但是,如果遇到下面两种情况该咋办呢?
-
有2个指针指向同一个节点。
-
多个线程要访问同一块共享内存。
编译期是无法检查到这些情况的,所以rust除了静态检查,还提供了运行时动态检查来满足这些特殊需求。
Rust的处理思路,大部分场景能在编译器处理,这样能保证安全性和性能要求;运行时检查,会牺牲部分性能,来提高灵活性
那Rust在运行时如何做检查呢?答案可能会令你比较失望:还是用到了引用计数(Rc Reference count)和智能指针(Arc Atomic reference counter)。我一开始以为不会用到引用计数这些招数呢!
引用计数 Rc
先看Rc,对一个数据结构T,我们可以创建引用计数Rc,让它有多个所有者。Rc会把对应的数据结构创建堆上。堆是唯一可以到处使用动态创建数据的内存。
use std::rc::Rc;
fn main() {
let a = Rc::new(1);
}
如果想对数据创建更多的所有者,我们可以通过clone()来完成。
对一个Rc结构进行clone(),不会将其内部的数据赋值,只会增加引用计数。当一个Rc结构离开作用域被drop()的时候,只会减少其引用计数,直到引用计数为0,才会真正清除对应的内存。
use std::rc::Rc;
fn main() {
let a = Rc::new(1);
let b = a.clone();
let c = a.clone();
}
这一坨代码里,我们有a,b,c3个 rc。它们指向堆上相同的数据,也就是说,堆上的数据有了三个共享的所有者。在这段代码结束的时候,c最先被drop,引用计数-1变为2,然后 b drop 引用计数变为1、a drop,引用计数归零,堆上内存被释放。
那么问题来了,这个教程之前给我们灌输的概念都是:一个值只能有一个所有者。但是现在a,b,c都对同一块内存有多个所有者,问题是编译器还没报 所有权冲突。
实际上a才是真正的所有者,b,c在clone()后,得到了一个新的Rc,从编译器的角度,a,b,c都各自拥有一个Rc。所以Rc的clone()并不复制实际的数据,只是把引用计数+1了。具体clone()代码如下:
fn clone(&self) -> Rc<T> {
// 增加引用计数
self.inner().inc_strong();
// 通过 self.ptr 生成一个新的 Rc 结构
Self::from_inner(self.ptr)
}
那么Rc是怎么在堆上产生的?且这段内存不受栈内存的生命周期所控制呢?这里就要提到 Box::leak机制了。
Box::leak机制
这种机制可以让Rust像C/C++那样,创建一块堆内存,且不受栈内存的控制,这样才能绕过编译器的所有权规则。
Box是Rust中的智能指针,可以强制吧数据创建在堆上,然后在栈上用一个指针指向这个数据结构,但这时候堆内存的生命周期是可控的,跟栈上的指针保持一致。
但是Box::leak可以从堆上泄露出去,不受栈内存的控制,是一个自由的、生命周期可以大到和整个进程一样的对象。有点类似C/C++里的malloc()分配的内存。
有了 Box::leak(),我们就可以跳出 Rust 编译器的静态检查,保证 Rc 指向的堆内存,有最大的生命周期,然后我们再通过引用计数,在合适的时机,结束这段内存的生命周期。(谁来结束呢?动态检查吗?最后一次清零的时候?)
搞明白了 Rc,我们就进一步理解 Rust 是如何进行所有权的静态检查和动态检查了:
-
静态检查,靠编译器保证代码符合所有权规则;
-
动态检查,通过 Box::leak 让堆内存拥有不受限的生命周期,然后在运行过程中,通过对引用计数的检查,保证这样的堆内存最终会得到释放。
到目前为止,只有Rc和接下来将学到的RefCell,Cell,Arc用到了动态检查,旗鱼都是静态检查。注意:动态检查仍然会校验所有权规则。
RefCell
Rc只是一个只读引用计数器,我们没有办法拿到Rc结构的内部数据的可变引用,来修改这个数据,因此需要RefCell来达成对只读数据的可变借用,称为内部可变性,Rc和RefCell可以搭配使用。
内部可变性和外部可变性
用mut 关键字声明的,明写着可以改的叫外部可变性。拧巴的情况又来了,有时候,想要对没有mut的值进行修改。也就是说在编译器眼里,这个值它是只读的,但是到运行时,这个值它是可以得到可变借用,修改其内部数据,这就是RefCell的用武之地。
上代码:
use std::cell::RefCell;
fn main() {
let data = RefCell::new(1);
{
// 获得 RefCell 内部数据的可变借用
let mut v = data.borrow_mut();
*v += 1;
}
println!("data: {:?}", data.borrow());
}
这一坨代码里,data并没有用mut 来声明,只是一个RefCell,但是在后面却可以用borrow_mut获得一个可变借用,把data里的值+1了。
注意:这里在可变借用的时候用一对{},这是因为使用 {} 缩短可变借用的生命周期。在同一个作用域下,不能同时拥有可变借用(borrow_mut)和不可变借用(borrow)
这就是外部可变性和内部可变性的重要区别,我们用下表来总结一下:
|
| 使用方法 | 所有权检查 |
| --- | --- | --- |
| 外部可变性 | let mut或者 &mut | 编译时,如不符合规则,产生编译错误 |
| 内部可变性 | 使用Cell/RefCell | 运行时,如不符合规则,产生panic |
本文使用 文章同步助手 同步