参考章节《Rust 程序设计语言》第15.5章 RefCell<T> 和内部可变性模式
《Rust 程序设计语言》上这一章,我实在是懒得吐槽,举的例子太啰嗦(复杂),这一章不建议看这本书
关于这一章,我推荐看下面这本开源书
参考章节《Rust语言圣经(Rust Course)》第3.4.5章 Cell 和 RefCell 内部可变性
内部可变性
好了我们先来引出问题
假如有一个外部库,它定义了一个消息发送器 Messenger,它有一个 send 方法,用于发送一条消息
|
|
我们要在自己的代码中实现这个 trait
出于
性能的考虑,消息先写到本地缓存(内存)中,然后批量发送出去,因此在send方法中,需要将消息先行插入到本地缓存msg_cache中。
但是问题来了,该 send 方法的签名是 &self
因为发送消息
不需要修改自身,因此原作者在定义时,使用了&self的不可变借用,这个无可厚非。
|
|
我们上面的代码不能通过编译,原因很简单,因为 &self 是不可变的,我们总不能让库的作者去修改他的代码吧
此时就可以利用 RefCell<T> 了
|
|
由于本身我们的send()方法是&self不可变引用,因此正常情况下,我们不能改变其内部成员的值,所以我们使用了RefCell<T>
RefCell<T>允许我们对一个不可变的值进行可变借用,所以我们在send()方法内部,获取了msg_cache的可变借用,从而使msg_cache可以修改
但它对于外部代码
仍然是不可变的
|
|
这种只在内部对一个不可变的值进行可变借用,叫做内部可变性,这是一种设计模式,它和RefCell<T>没有必然联系,但RefCell<T>实现了内部可变性
什么叫做RefCell<T>实现了内部可变性? 请看如下代码
|
|
虽然 counter 是不可变的,不过我们仍然可以获取其内部值的可变引用,所以说RefCell<T>实现了内部可变性
RefCell
接下来我们来看看 RefCell 到底做了什么事情
首先,RefCell<T> 内部使用了不安全的代码来模糊Rust的借用检查规则,这一点知道就行了,我们还没有学习到 unsafe
其次,RefCell<T> 将检查借用规则步骤从编译时期转移到了运行时期
我们回忆一下借用规则
- 在任何给定时间里,要么只能拥有
一个可变引用,要么只能拥有任意数量的不可变引用- 引用必须
总是有效的
在编译期检查借用规则,当不满足时出现错误,而在运行时检查借用规则,如过不满足,则panic
下图展示了Box、Rc、RefCell之间的区别,它出自杨旭的Rust视频

好了知道了这些后,我们来看看,上面的代码,为什么我们使用 RefCell<T> 后,程序就能编译通过了
首先在上面的定义中
msg_cache本身是不可变的(因为实例引用是&self)这一点没问题吧
然后我们通过borrow_mut()获取了一个msg_cache可变借用,但根据借用规则是不允许可变引用和不可变引用共存的
这种情况下正常是不允许编译的(不允许你获取它的可变引用),但RefCell<T>将检查步骤从编译时转移到了运行时,此时程序就能正常编译通过了
Rc和RefCell组合使用
下面的例子是原封不动照搬《Rust语言圣经》,因为我觉得这个例子举的很好理解,比《Rust 程序设计语言》中的例子好的多
在 Rust 中,一个常见的组合就是 Rc 和 RefCell 在一起使用
|
|
上面代码中,我们使用 RefCell<String> 包裹一个字符串,同时通过 Rc 创建了它的三个所有者:s、s1和s2,并且通过其中一个所有者 s2 对字符串内容进行了修改。
由于 Rc 的所有者们共享同一个底层的数据,因此当一个所有者修改了数据时,会导致全部所有者持有的数据都发生了变化。
运行结果
|
|
总结一下
内部可变性是一种设计模式RefCell<T>允许我们对一个不可变的值进行可变借用RefCell<T>将借用规则检查从编译期转移到了运行时RefCell<T>只是转移了借用规则检查的时机,但并不是不检查了,如果不满足要求则会触发panic
例如下面这样是不允许的,这将触发
panic
|
|