15.6 RefCell与内部可变性:“摆脱”安全性限制
15.6.1 什么是内部可变性
内部可变性(interior mutability)是 Rust 的设计模式之一。它允许程序员在只持有不可变引用的前提下修改数据。
通常而言,这样的行为会被借用规则(详见 4.4. 引用与借用)所禁止,但为了能够改变数据,内部可变性模式会在数据结构内部使用 unsafe 代码,来绕过 Rust 正常的可变性和借用规则。
不安全代码告诉编译器:我们自己检查规则,而不是依赖编译器替我们检查。与不安全代码相关的概念将在以后的文章中涉及。
15.6.2 RefCell<T>
与 Rc<T> 不同,RefCell<T> 表示它所持有数据的唯一所有权。
为了理解 RefCell<T> 与 Box<T> 的区别,我们需要回顾借用规则(详见 4.4. 引用与借用):
- 在任意给定时刻,你要么拥有一个可变引用,要么拥有任意数量的不可变引用,但不能同时拥有两者。
- 引用总是有效的。
PS:“任意给定时刻”可以理解为“在给定作用域内”。
RefCell<T> 与 Box<T> 的区别如下:
| 类型 | 检查阶段 | 违反规则的结果 |
|---|---|---|
Box<T> | 编译时检查借用规则 | 编译时报错 |
RefCell<T> | 运行时检查借用规则 | 触发 panic |
在不同阶段检查借用规则具有不同特点:
-
编译时:
- 尽早暴露问题
- 没有运行时开销
- 是大多数场景的最佳选择
- 是 Rust 的默认行为
-
运行时:
- 问题暴露较晚,甚至可能到生产环境
- 因借用跟踪产生少量性能开销
- 使某些内存安全场景成为可能,例如在不可变环境中修改数据
该在什么时候使用 RefCell<T>?
Rust 编译器会在编译时检查所有代码。它能理解大部分代码;如果没有问题就通过编译,如果有问题就报错。
Rust 编译器非常保守。有些代码无法在编译时被完整分析,Rust 会直接拒绝这类代码,即使它实际上是正确的。
Rust 如此保守是为了保证安全性。虽然拒绝完全正确的代码会给开发者带来不便,但可以避免灾难性后果。
如果编译器无法分析某段代码,但开发者能够保证这段代码满足借用规则,那么 RefCell<T> 就是一个好选择。
与 RefCell<T> 类似,Rc<T> 只适用于单线程场景。
15.6.3 如何在 Box<T>、Rc<T> 和 RefCell<T> 中选择
可以根据下表列出的特性在三者之间选择:
| 特性 | Box<T> | Rc<T> | RefCell<T> |
|---|---|---|---|
| 同一数据的所有权 | 一个所有者 | 多个所有者 | 一个所有者 |
| 可变性 / 借用检查 | 可变与不可变借用(编译时检查) | 不可变借用(编译时检查) | 可变与不可变借用(运行时检查) |
额外说一句:因为 RefCell<T> 只在运行时检查,所以即使 RefCell<T> 本身是不可变的,我们仍然可以修改存储在其中的值。
15.6.4 内部可变性:可变地借用一个不可变的值
这个标题有一点绕。它的意思是:对一个没有声明为 mut 的类型使用 &mut 引用。看个例子就明白了:
fn main() {
let x = 5;
let y = &mut x;
}
借用规则的一个推论是:当你有一个不可变的值时,就不能可变地借用它。所以这段代码会报错:
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
--> src/main.rs:3:13
|
3 | let y = &mut x;
| ^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
2 | let mut x = 5;
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing` (bin "borrowing") due to 1 previous error
然而,在某些特定情况下,我们需要这样一个值:对外保持不可变,但可以在自己的方法内部修改自身;除了该值自身的方法外,其他代码都不能修改它。这叫做内部可变性。RefCell<T> 正是为这种情况而存在的。
但 RefCell<T> 并没有完全绕开借用规则。虽然编译时检查可以通过,但在运行时违反借用规则仍会导致程序 panic。
下面看一个例子(lib.rs):
功能:跟踪某个值与最大值的接近程度,并在该值达到特定级别时发出警告
#![allow(unused)]
fn main() {
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
}
这个例子的逻辑并不重要,看一下它的结构:
-
程序开头定义了
Messengertrait,其中包含send方法的签名:接收&self和一个类型为&str的字符串切片参数msg。 -
下面定义了一个名为
LimitTracker的结构体。它是一个带有生命周期'a和泛型参数T的泛型类型,其中T的生命周期为'a,并且必须实现程序开头定义的Messengertrait。LimitTracker有三个字段:messenger:类型为&'a Tvalue:类型为usizemax:类型为usize
-
接着,一个
impl块为LimitTracker定义了关联函数new。其参数是类型为&T的messenger和类型为usize的max,返回一个LimitTracker。这个函数创建一个LimitTracker实例,其中:messenger字段是messenger参数的值value字段是0max字段是max参数的值
-
LimitTracker还有一个名为set_value的方法。其第一个参数是对self的可变引用&mut self,第二个参数是value,类型为usize。 方法内部的逻辑很简单。它把self.value字段除以self.max字段(两者都转换为f64以避免丢失精度)得到一个百分比,存入percentage_of_max,并根据percentage_of_max的大小,使用Messengertrait 的send方法发送不同的警告。
使用测试替身(test double)进行测试
这里有一个问题。如果要测试这个 set_value 方法,就需要该方法输出一些可供断言的内容。但 set_value 实际上不返回任何值,因此不会提供任何可用于断言的结果。
我们要测试的是:当我们用一个实现了 Messenger trait 的值和一个 max 值创建 LimitTracker 实例时,传入不同的 value 会触发 Messenger 发送不同的消息。
为了解决这个问题,这里介绍测试替身(test double),中文也叫测试替代。它是一个通用的测试概念,表示测试中使用的替代品。在测试替身中,有一种特定类型叫做模拟对象(Mock Object),负责记录测试过程中发生的事情。我们可以利用这些记录来断言测试是否正确运行。
Rust 没有内置的等价物,标准库也不提供模拟对象,但我们可以自定义一个结构体来做同样的事情。
接着上面的代码继续写:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
struct MockMessenger {
sent_messages: Vec<String>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: vec![],
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.len(), 1);
}
}
}
-
在测试模块开头,我们声明一个
MockMessenger结构体,它有一个字段sent_messages,用于存储已发送的消息,类型为Vec<String>。 -
然后通过
impl块为MockMessenger添加new函数,用于创建MockMessenger实例。sent_messages字段初始化为一个空的Vector。 -
接着为
MockMessenger实现程序开头定义的Messengertrait。实现该 trait 后,就可以用MockMessenger创建LimitTracker,因为LimitTracker要求其泛型类型实现Messenger。 当调用send方法时,消息会被存入MockMessenger的sent_messages这个Vector中。 -
最后,
it_sends_an_over_75_percent_warning_message测试函数测试超过 75% 的情况。 首先创建名为mock_messenger的MockMessenger实例,再创建名为limit_tracker的LimitTracker实例,然后在该LimitTracker实例上调用方法。最后通过检查mock_messenger.sent_messages中的元素数量来断言。
此时代码逻辑有问题,运行会报错:
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
--> src/lib.rs:58:13
|
58 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so it cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference in the `impl` method and the `trait` definition
|
2 ~ fn send(&mut self, msg: &str);
3 | }
...
56 | impl Messenger for MockMessenger {
57 ~ fn send(&mut self, message: &str) {
|
For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` (lib test) due to 1 previous error
错误出现在为 MockMessenger 实现 send 方法时:
#![allow(unused)]
fn main() {
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.push(String::from(message));
}
}
}
我们无法修改 MockMessenger 来跟踪消息,因为 send 方法的签名接收的是对 self 的不可变引用。我们也不能改成 &mut self,因为那样 send 的签名就与 Messenger trait 中定义的 &self 签名不匹配了。
针对这种需要内部可变性的情况,可以使用 RefCell<T>。我们只需要把 MockMessenger 的 sent_messages 字段用 RefCell<T> 包起来:
#![allow(unused)]
fn main() {
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
}
因为 RefCell<T> 不在预导入模块中,所以使用前需要先引入作用域:
#![allow(unused)]
fn main() {
use std::cell::RefCell;
}
改完之后,每一处使用 sent_messages 字段的代码也需要用 RefCell<T> 包装:
#![allow(unused)]
fn main() {
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
}
RefCell 该怎么用呢?用 RefCell 创建的数据可以通过 borrow_mut 方法修改。对参数调用 borrow_mut 会得到可变引用,因此 MockMessenger 的 send 方法可以使用 borrow_mut:
#![allow(unused)]
fn main() {
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
}
}
}
这样一来,即使 send 的参数是不可变引用,也可以在函数体内部通过 borrow_mut 修改值。
最后修改测试函数中的断言:
#![allow(unused)]
fn main() {
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
对 mock_messenger 使用 borrow,即可获得该值的不可变引用用于断言。
现在代码没有任何问题。完整代码如下:
#![allow(unused)]
fn main() {
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger
.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
}
15.6.5 使用 RefCell<T> 在运行时记录借用信息
实际上,上文使用的 borrow_mut 和 borrow 方法就像提供给用户的两个安全接口:
borrow返回智能指针Ref<T>,它实现了Dereftraitborrow_mut返回智能指针RefMut<T>,它实现了Deref和DerefMuttrait
RefCell<T> 会跟踪当前存在多少活跃的 Ref<T> 和 RefMut<T>:
- 每次调用
borrow,不可变借用计数加1。 当任何一个Ref<T>值离开作用域并被丢弃时,不可变借用计数减1。 - 每次调用
borrow_mut,可变借用计数加1。 当任何一个RefMut<T>值离开作用域并被丢弃时,可变借用计数减1。
与编译时借用规则(详见 4.4. 引用与借用)一样,RefCell<T> 允许我们在任意时刻拥有许多不可变借用,或一个可变借用。
如果我们尝试违反这些规则,RefCell<T> 的实现会在运行时 panic,因为 RefCell<T> 在运行时检查借用规则。panic 信息 already borrowed: BorrowMutError 就是 RefCell<T> 在运行时处理借用规则违反的方式。
15.6.6 将 Rc<T> 和 RefCell<T> 结合使用的例子
Rc<T> 允许某些数据被多个所有者持有,但它只提供对该数据的不可变访问。如果你有一个包含 RefCell<T> 的 Rc<T>,就可以得到一个拥有多个所有者并且可变的值。
下面看一个将 Rc<T> 和 RefCell<T> 结合使用、以实现多重所有权可变数据的例子:
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let value = Rc::new(RefCell::new(5));
let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));
*value.borrow_mut() += 10;
println!("a after = {a:?}");
println!("b after = {b:?}");
println!("c after = {c:?}");
}
还记得上一篇文章中的 Cons 列表示例吗?我们使用 Rc<T> 允许多个列表共享另一个列表的所有权。因为 Rc<T> 只保存不可变值,一旦创建了列表中的任何值,就无法再更改它们。结合本文所学,我们再加上 RefCell<T>,以便能够修改列表中的值:
- 首先,在定义
List枚举时,把Cons关联的i32用RefCell<>包裹,以便可以修改该值。外面再用Rc<>包裹,以便多个所有者可以共享它,其余保持不变。 - 记得把
Rc和RefCell引入当前作用域。 - 然后用
Rc::new()和RefCell::new()创建实例。a通过Rc::clone()共享value的值,b和c通过Rc::clone()共享a的值(前提是a被Rc<>包裹)。 - 最后,对
RefCell<T>使用borrow_mut获得对value的可变借用。再用解引用运算符*像操作i32一样给它加10。
输出:
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))
跟预期一样,没有问题。
15.6.7 其他可以实现内部可变性的类型
Cell<T>:通过复制来访问数据Mutex<T>:用于在多线程上下文中实现内部可变性