15.5 RefCell
RefCell<T> 和内部可变性模式¶
ch15-05-interior-mutability.md
内部可变性(Interior mutability)是 Rust 中的一个设计模式,它允许你即使在有不可变引用时也可以改变数据,这通常是借用规则所不允许的。为了改变数据,该模式在数据结构中使用 unsafe 代码来模糊 Rust 通常的可变性和借用规则。不安全代码表明我们在手动检查这些规则而不是让编译器替我们检查。第二十章会更详细地介绍不安全代码。
当可以确保代码在运行时会遵守借用规则,即使编译器不能保证的情况,可以选择使用那些运用内部可变性模式的类型。所涉及的 unsafe 代码将被封装进安全的 API 中,而外部类型仍然是不可变的。
让我们通过遵循内部可变性模式的 RefCell<T> 类型来探索这个概念。
在运行时强制借用规则¶
不同于 Rc<T>,RefCell<T> 代表其数据的唯一的所有权。那么是什么让 RefCell<T> 不同于像 Box<T> 这样的类型呢?回忆一下第四章所学的借用规则:
- 在任意给定时刻,只能拥有一个可变引用或任意数量的不可变引用之一(而不是两者)。
- 引用必须始终有效。
对于引用和 Box<T>,借用规则的不可变性(invariants)在编译时就会被强制执行。对于 RefCell<T>,这些不可变性作用于运行时。对于引用,如果违反这些规则,会得到一个编译错误。而对于 RefCell<T>,如果违反这些规则程序会 panic 并退出。
在编译时检查借用规则的优势是这些错误将在开发过程的早期被捕获,同时对运行时没有性能影响,因为所有的分析都提前完成了。为此,在编译时检查借用规则是大部分情况的最佳选择,这也正是其为何是 Rust 的默认行为。
相反在运行时检查借用规则的好处则是允许出现特定内存安全的场景,而它们在编译时检查中是不允许的。静态分析,正如 Rust 编译器,是天生保守的。但代码的一些属性不可能通过分析代码发现:其中最著名的就是停机问题(Halting Problem),这超出了本书的范畴,不过如果你感兴趣的话这是一个值得研究的有趣主题。
因为一些分析是不可能的,如果 Rust 编译器不能通过所有权规则编译,它可能会拒绝一个正确的程序;从这种角度考虑它是保守的。如果 Rust 接受不正确的程序,那么用户也就不会相信 Rust 所做的保证了。然而,如果 Rust 拒绝正确的程序,虽然会给程序员带来不便,但不会带来灾难。RefCell<T> 正是用于当你确信代码遵守借用规则,而编译器不能理解和确定的时候。
类似于 Rc<T>,RefCell<T> 只能用于单线程场景。如果尝试在多线程上下文中使用 RefCell<T>,会得到一个编译错误。第十六章会介绍如何在多线程程序中实现 RefCell<T> 的功能。
如下为选择 Box<T>,Rc<T> 或 RefCell<T> 的理由:
Rc<T>允许相同数据有多个所有者;Box<T>和RefCell<T>则只有单一所有者。Box<T>允许在编译时执行不可变或可变借用检查;Rc<T>仅允许在编译时执行不可变借用检查;RefCell<T>允许在运行时执行不可变或可变借用检查。- 因为
RefCell<T>允许在运行时执行可变借用检查,所以我们可以在即便RefCell<T>自身是不可变的情况下修改其内部的值。
在不可变值内部改变值就是内部可变性(interior mutability)模式。让我们看看何时内部可变性是有用的,并讨论这是如何成为可能的。
使用内部可变性¶
借用规则的一个推论是当有一个不可变值时,不能可变地借用它。例如,如下代码不能编译:
```rust,ignore,does_not_compile {{#rustdoc_include ../listings/ch15-smart-pointers/no-listing-01-cant-borrow-immutable-as-mutable/src/main.rs}}
如果尝试编译,会得到如下错误:
```console
{{#include ../listings/ch15-smart-pointers/no-listing-01-cant-borrow-immutable-as-mutable/output.txt}}
然而,特定情况下,令一个值在其方法内部能够修改自身,而在其他代码中仍视为不可变,是很有用的。值方法外部的代码就不能修改其值了。RefCell<T> 是一个获得内部可变性的方法。RefCell<T> 并没有完全绕开借用规则,编译器中的借用检查器允许内部可变性并相应地在运行时检查借用规则。如果违反了这些规则,会出现 panic 而不是编译错误。
让我们通过一个实际的例子来探索何处可以使用 RefCell<T> 来修改不可变值并看看为何这么做是有意义的。
使用 mock 对象测试¶
有时在测试中程序员会用某个类型替换另一个类型,以便观察特定的行为并断言它是被正确实现的。这个占位符类型被称为 测试替身(test double)。就像电影制作中的替身演员(stunt double)一样,替代演员完成高难度的场景。测试替身在运行测试时替代某个类型。mock 对象 是特定类型的测试替身,它们记录测试过程中发生了什么以便可以断言操作是正确的。
Rust 并不像其他语言那样在标准库中提供内建的对象模型,Rust 也没有像其他语言那样在标准库中内建 mock 对象功能,不过我们确实可以创建一个与 mock 对象有着相同功能的结构体。
如下是一个我们想要测试的场景:我们在编写一个记录某个值与最大值的差距的库,并根据当前值与最大值的差距来发送消息。例如,这个库可以用于记录用户所允许的 API 调用数量限额。
该库只提供记录与最大值的差距,以及何种情况发送什么消息的功能。使用此库的程序则期望提供实际发送消息的机制:程序可以选择记录一条消息、发送 email、发送短信等等。库本身无需知道这些细节;只需实现其提供的 Messenger trait 即可。示例 15-20 展示了库代码:
文件名:src/lib.rs
```rust,noplayground {{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-20/src/lib.rs}}
<span class="caption">示例 15-20:一个记录某个值与最大值差距的库,并根据此值的特定级别发出警告</span>
这些代码中一个重要部分是拥有一个方法 `send` 的 `Messenger` trait,其获取一个 `self` 的不可变引用和文本信息。这个 trait 是 mock 对象所需要实现的接口库,这样 mock 就能像一个真正的对象那样使用了。另一个重要的部分是我们需要测试 `LimitTracker` 的 `set_value` 方法的行为。可以改变传递的 `value` 参数的值,不过 `set_value` 并没有返回任何可供断言的值。我们希望能够说,如果我们创建一个实现了 `Messenger` trait 和具有特定 `max` 值的 `LimitTracker` 时,当传递不同 `value` 值时,消息发送者应被告知发送合适的消息。
我们所需的 mock 对象是,调用 `send` 并不实际发送 email 或消息,而是只记录信息被通知要发送了。可以新建一个 mock 对象实例,用其创建 `LimitTracker`,调用 `LimitTracker` 的 `set_value` 方法,然后检查 mock 对象是否有我们期望的消息。示例 15-21 展示了一个如此尝试的 mock 对象实现,不过借用检查器并不允许:
<span class="filename">文件名:src/lib.rs</span>
```rust,ignore,does_not_compile
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-21/src/lib.rs:here}}
示例 15-21:尝试实现 MockMessenger,借用检查器不允许这么做
测试代码定义了一个 MockMessenger 结构体,其 sent_messages 字段为一个 String 值的 Vec 用来记录被告知发送的消息。我们还定义了一个关联函数 new 以便于新建从空消息列表开始的 MockMessenger 值。接着为 MockMessenger 实现 Messenger trait 这样就可以为 LimitTracker 提供一个 MockMessenger。在 send 方法的定义中,获取传入的消息作为参数并储存在 MockMessenger 的 sent_messages 列表中。
在测试中,我们测试了当 LimitTracker 被告知将 value 设置为超过 max 值 75% 的某个值。首先新建一个 MockMessenger,其从空消息列表开始。接着新建一个 LimitTracker 并传递新建 MockMessenger 的引用和 max 值 100。我们使用值 80 调用 LimitTracker 的 set_value 方法,这超过了 100 的 75%。接着断言 MockMessenger 中记录的消息列表应该有一条消息。
然而,这个测试存在一个问题,如下所示:
不能修改 MockMessenger 来记录消息,因为 send 方法接收的是对 self 的不可变引用。我们也不能采纳错误提示中将 &self 改为 &mut self 的建议,因为那样既要在 impl 方法中修改签名,也要在 Messenger trait 定义中修改签名。我们并不希望仅为了测试而改变 Messenger trait。相反,我们需要想办法让测试代码与现有设计兼容,正常工作。
这正是内部可变性可以派上用场的地方!我们会把 sent_messages 存储在一个 RefCell<T> 里,然后 send 方法就能修改 sent_messages 来保存我们见到的消息。示例 15-22 展示了它的写法:
文件名:src/lib.rs
```rust,noplayground {{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-22/src/lib.rs:here}}
<span class="caption">示例 15-22:使用 `RefCell<T>` 能够在外部值被认为是不可变的情况下修改内部值</span>
现在 `sent_messages` 字段的类型是 `RefCell<Vec<String>>` 而不是 `Vec<String>`。在 `new` 函数中在空 vector 外层创建了一个 `RefCell<Vec<String>>` 实例。
对于 `send` 方法的实现,第一个参数仍为 `self` 的不可变借用,这是符合 trait 定义的。我们调用 `self.sent_messages` 中 `RefCell<Vec<String>>` 的 `borrow_mut` 方法来获取 `RefCell<Vec<String>>` 中值的可变引用,这是一个 vector。接着可以对 vector 的可变引用调用 `push` 以便记录测试过程中看到的消息。
最后必须做出的修改位于断言中:为了看到其内部 vector 中有多少个项,需要调用 `RefCell<Vec<String>>` 的 `borrow` 以获取 vector 的不可变引用。
现在我们见识了如何使用 `RefCell<T>`,让我们研究一下它怎样工作的!
### 在运行时记录借用
当创建不可变和可变引用时,我们分别使用 `&` 和 `&mut` 语法。对于 `RefCell<T>` 来说,则是 `borrow` 和 `borrow_mut` 方法,这属于 `RefCell<T>` 安全 API 的一部分。`borrow` 方法返回 `Ref<T>` 类型的智能指针,`borrow_mut` 方法返回 `RefMut<T>` 类型的智能指针。这两个类型都实现了 `Deref`,所以可以当作常规引用对待。
`RefCell<T>` 记录当前有多少个活动的 `Ref<T>` 和 `RefMut<T>` 智能指针。每次调用 `borrow`,`RefCell<T>` 将活动的不可变借用计数加一。当 `Ref<T>` 值离开作用域时,不可变借用计数减一。就像编译时借用规则一样,`RefCell<T>` 在任何时候只允许有多个不可变借用或一个可变借用。
如果我们尝试违反这些规则,相比引用时的编译时错误,`RefCell<T>` 的实现会在运行时出现 panic。示例 15-23 展示了对示例 15-22 中 `send` 实现的修改,这里我们故意尝试在相同作用域创建两个可变借用以便演示 `RefCell<T>` 不允许我们在运行时这么做。
<span class="filename">文件名:src/lib.rs</span>
```rust,ignore,panics
{{#rustdoc_include ../listings/ch15-smart-pointers/listing-15-23/src/lib.rs:here}}
示例 15-23:在同一作用域中创建两个可变引用并观察 RefCell<T> 将会 panic
这里为 borrow_mut 返回的 RefMut 智能指针创建了 one_borrow 变量。接着用相同的方式在变量 two_borrow 创建了另一个可变借用。这会在相同作用域中创建两个可变引用,这是不允许的。当运行库的测试时,示例 15-23 编译时不会有任何错误,不过测试会失败:
注意代码 panic 和信息 already borrowed: BorrowMutError。这也就是 RefCell<T> 如何在运行时处理违反借用规则的情况。
像这里这样选择在运行时而不是编译时捕获借用错误,意味着你可能会在开发过程的更后期才发现错误,甚至直到代码部署到生产环境后才暴露出来。与此同时,你的代码还会因为在运行时而不是编译时跟踪借用而承担一点运行时性能开销。不过,使用 RefCell<T> 能让我们在只允许不可变值的上下文中,写出一个可以修改自身来记录消息的 mock 对象。尽管存在这些权衡,RefCell<T> 仍能提供比常规引用更多的能力。
允许多个可变数据所有者¶
RefCell<T> 的一个常见用法是与 Rc<T> 结合。回忆一下 Rc<T> 允许对相同数据有多个所有者,不过只能提供数据的不可变访问。如果有一个储存了 RefCell<T> 的 Rc<T> 的话,就可以得到有多个所有者并且可以修改的值了!
例如,回忆示例 15-18 中的 cons list 例子,我们使用 Rc<T> 让多个列表共享另一个列表的所有权。由于 Rc<T> 只持有不可变值,所以一旦创建这些列表之后,就无法再修改其中的值。现在让我们加入 RefCell<T>,借助它来修改列表中的值。示例 15-24 展示了:通过在 Cons 定义中使用 RefCell<T>,我们就能修改所有列表中存储的值:
文件名:src/main.rs
示例 15-24:使用 Rc<RefCell<i32>> 创建可以修改的 List
这里创建了一个 Rc<RefCell<i32>> 实例并储存在变量 value 中以便之后直接访问。接着在 a 中用包含 value 的 Cons 变体创建了一个 List。需要克隆 value 以便 a 和 value 都能拥有其内部值 5 的所有权,而不是将所有权从 value 移动到 a 或者让 a 借用 value。
我们将列表 a 封装进了 Rc<T> 这样当创建列表 b 和 c 时,它们都可以引用 a,正如示例 15-18 一样。
一旦创建了列表 a、b 和 c,我们将 value 的值加 10。为此对 value 调用了 borrow_mut,这里使用了第五章讨论的自动解引用功能(“-> 运算符到哪去了?” 部分)来解引用 Rc<T> 以获取其内部的 RefCell<T> 值。borrow_mut 方法返回 RefMut<T> 智能指针,可以对其使用解引用运算符并修改其内部值。
当我们打印出 a、b 和 c 时,可以看到它们都拥有修改后的值 15 而不是 5:
这个技巧相当巧妙!通过使用 RefCell<T>,我们可以拥有一个对外看起来不可变的 List 值,但在需要时仍然能够使用 RefCell<T> 提供的内部可变性方法来修改数据。借用规则在运行时进行检查,这也确实保护了我们免受数据竞争的影响;有时候,为了换取数据结构上的这种灵活性,付出一点性能代价是值得的。注意,RefCell<T> 不适用于多线程代码!Mutex<T> 是 RefCell<T> 的线程安全版本,我们会在第十六章讨论 Mutex<T>。