https://www.bilibili.com/video/BV1AAaxeSEHU/
哈喽大家好,这里是曼苏。今天主要是把上一期内容重新录一下,上一期背景音乐有点大,人声都听不清楚。内容主要还是讲内部可变性(interior mutability)。
我们重新创建一个文件,先把原来那个文件改个名字,然后再重新创建一个文件。我们来运行一下看看,好,现在是这个新文件了。
我们要谈的主要是Rust标准库里面的这三个struct:Cell、RefCell和OnceCell。
首先说说什么是interior mutability。简单来说,我们知道在Rust里面,当你这样定义一个变量:
1 | let a = 5; |
你是不可能再去改变a的值的。只有把它定义成mut才可以改变这个值:
1 | let mut b = 5; |
在这里你可以改变b的值,但你不能改变a的值。
那么interior mutability就是说,在我把这个变量还是像a的这种定义方法,但是我仍然能够改变它内部的一些值,这个就叫interior mutability。
在Rust的官方文档里,在Cell的文档中已经给了一个例子,很好地说明了这个问题。比如他在官方文档里面的例子是这样的:
1 | struct SomeStruct { |
当我把special_field定义成Cell这种类型的时候,我们就可以这么做:
1 | let mut val = SomeStruct { |
这里的val我们没有加mut关键字,但是我们可以去改变这个special_field的值。
我们来运行一下结果看一下。OK,结果都是通过的,就说明这个special_field确实改变了。
这个例子很好地解释了interior mutability这个概念是什么意思。就是我们看到这个SomeStruct,它并不是一个mut类型,但是在后面我们却可以通过Cell这个特殊的包装结构,去改变它的值。
具体来说,这个有什么用呢?因为官方给的这个例子比较抽象,我们可以给一个更贴近生活、更贴近实际的例子。
比如说我们有一个手机型号的结构体,假设你是做一个卖手机的电商网站,里面有很多不同的品牌,每个不同的品牌也有不同的手机型号。那么我们就可以这样定义:
1 | struct PhoneModel { |
这里面,brand、model_name和release_date这三个字段一旦定义后就不会再改变。但是on_sale这个字段可能会随着时间的变化而改变。在实际应用中,我们就可以通过interior mutability来定义这样一个数据结构,从而应对实际中会产生的这种情况。
我们可以简单写一个例子:
1 | let abc_phone = PhoneModel { |
我们来运行看一下,全部都通过了,就说明这个时候我们确实改变了这个on_sale值。可以通过这种方法在同样的PhoneModel结构体中,它前面没有mut关键字,但是我依然改变了它内部的一些部分数据。
接下来讲RefCell。RefCell的话,简单来说就是因为你大家去看这个Cell的文档,就会发现它这里面get值,或者我们自己看这个get这个函数,你看它实际上是复制了里面的值出来。你看他这个get他要求这个T是Copy trait,也就是说我每次get的时候,我是把这个Cell里面包的这个值复制了一份出来。
那这样的话对于一些基本类型,这个是没问题的,比如说整型、布尔型、浮点型,这些他是没问题,因为它本身就是Rust的这些基本类型本身就是定义了Copy。那如果说你这个地方是一个非常复杂的、另外一个结构体,比如说你这里是一个HashMap,比如说你这里是一个没有定义Copy这个trait的类型,那你每次去get,然后都要把它复制一份,如果你这个数据量很大,其实这就不是一个很有效的操作。
那这个时候就可以把它定义成RefCell。我们还是用这个PhoneModel的例子来进行说明。比如说我们这里定义一个车的类型:
1 | struct CarModel { |
这里的parameters可能包含很多选配,比如不同的颜色、轮胎大小等。
这个时候如果你把它定义成Cell,那就行不通了。我们来看看Cell它能不能进行后续的一些操作:
1 | let car_model = CarModel { |
这里会报错,因为HashMap并没有定义Copy这个trait。所以对于这种相对来讲比较复杂的情况,你就不能用Cell了,这个时候就要用RefCell。
1 | let car_model = CarModel { |
这样就可以了。
这里面要讲的一个就是这个borrow和borrow_mut。RefCell的borrow checking是在运行时进行的,而不是在编译阶段。也就是说,如果这里有问题,实际上我这里应该会出现错误的提示,但是RefCell的borrow和borrow_mut,它并不是在编译阶段检查的,而是在运行状态时检查的。
我们再来回顾一下这个错误:
1 | let mut_borrow = car_model.parameters.borrow_mut(); |
它就会报错说already mutably borrowed。
RefCell的borrow checking还是符合Rust的一些规则,比如说:
- 你可以同时有多个不可变借用:
1 | let borrow1 = car_model.parameters.borrow(); |
- 但是你只能有一个可变借用,而且在有可变借用的时候不能有不可变借用:
1 | let mut_borrow = car_model.parameters.borrow_mut(); |
最后说一下OnceCell。OnceCell的应用场景是什么呢?它是用于那些初始化需要很长时间的数据结构。比如说你要从磁盘里面读进一个很大的文件,但是其实你这个文件你用到这个文件,你是要看你程序执行的情况。有时候你的程序遇到的情况,未必要读取这个文件。那你当你在初始化的时候,你读取了这个文件,但这个文件在后续又没用到,那这就是一个不是那么有效的操作,你可能耽误了大量的时间去读取那个文件。
OnceCell就是说当你不用它的时候,它就没有初始化,它就是一个相当于是一个空值。当你需要用到它的时候,它才会去进行这个初始化。这有点像其他语言中的lazy loading。
比如说我们有一个很大的数据集:
1 | fn main() { |
在这个例子中,只有当n大于100时,我们才会初始化并使用这个大数组。这样就有效地避免了在不需要使用大数组的情况下初始化它,从而节省了内存和CPU资源。
这就是Cell、RefCell和OnceCell的主要功能和区别。
好,那今天就这样,就先到这里。谢谢大家的关注,下次见。
原文链接: https://dashen.tech/2018/08/06/Rust-编程实战-Cell,RefCell,-OnceCell/
版权声明: 转载请注明出处.