RefCell<T>
和内部可变性模式
内部可变性是 Rust 中的一种设计模式,允许你进行 mut
data 即使存在对该数据的不可变引用;通常,这个
作 不允许借用规则。为了改变数据,该模式使用
unsafe
代码来改变 Rust 控制 mutation 和 borrowing 的常用规则。不安全代码向编译器表明我们正在手动检查规则,而不是依赖编译器为我们检查规则;我们将在第 19 章中更多地讨论不安全代码。
只有当我们可以确保在运行时遵循借用规则时,我们才能使用使用内部可变性模式的类型,即使编译器无法保证这一点。然后,涉及的不安全
代码被包装在一个安全的 API 中,并且外部类型仍然是不可变的。
让我们通过查看遵循内部可变性模式的 RefCell<T>
类型来探索这个概念。
使用 RefCell<T>
在运行时强制执行借用规则
与 Rc<T>
不同,RefCell<T>
类型表示对其所保存的数据的单一所有权。那么,RefCell<T>
与像 Box<T>
这样的类型有什么不同呢?回想一下你在第 4 章中学到的借用规则:
在任何给定时间,你可以有一个(但不能同时拥有)一个可变引用或任意数量的不可变引用。
引用必须始终有效。
使用 references 和 Box<T>
,借用规则的不变量在编译时强制执行。使用 RefCell<T>
,这些不变量在运行时强制执行。
对于引用,如果你违反了这些规则,你将得到一个编译器错误。跟
RefCell<T>
,如果您违反了这些规则,您的程序将崩溃并退出。
在编译时检查借用规则的优点是,在开发过程中可以更快地捕获错误,并且由于所有分析都是事先完成的,因此不会影响运行时性能。由于这些原因,在大多数情况下,在编译时检查借用规则是最好的选择,这就是为什么这是 Rust 的默认选择。
相反,在运行时检查借用规则的优点是,允许某些内存安全的场景,而编译时检查则不允许这些场景。静态分析,就像 Rust 编译器一样,本质上是保守的。代码的某些属性无法通过分析代码来检测:最著名的例子是 Halting Problem,这超出了本书的范围,但是一个有趣的研究主题。
因为某些分析是不可能的,所以如果 Rust 编译器无法确定代码符合所有权规则,它可能会拒绝正确的程序;从这个角度来看,它是保守的。如果 Rust 接受了错误的程序,用户将无法信任 Rust 所做的保证。但是,如果 Rust 拒绝正确的程序,程序员会感到不便,但不会发生灾难性的事情。当您确定代码遵循借用规则,但编译器无法理解和保证这一点时,RefCell<T>
类型非常有用。
与 Rc<T>
类似,RefCell<T>
仅用于单线程方案,如果尝试在多线程上下文中使用它,则会出现编译时错误。我们将在第 16 章讨论如何在多线程程序中获得 RefCell<T>
的功能。
以下是选择 Box<T>
、Rc<T>
或 RefCell<T>
的原因回顾:
Rc<T>
支持同一数据的多个所有者;Box<T>
和RefCell<T>
拥有单一所有者。Box<T>
允许在编译时检查不可变或可变借用;Rc<T>
只允许在编译时检查不可变的借款;RefCell<T>
允许在运行时检查不可变或可变借用。
由于RefCell<T>
允许在运行时检查可变借用,因此即使RefCell<T>
是不可变的,您也可以更改RefCell<T>
中的值。
在不可变值中改变值是内部可变性
模式。让我们看看内部可变性有用的情况,并且
检查它是如何可能的。
内部可变性:对不可变值的 Mutable Borrow
借用规则的结果是,当你有一个不可变的值时,你不能可变地借用它。例如,此代码不会编译:
fn main() {
let x = 5;
let y = &mut x;
}
如果您尝试编译此代码,则会收到以下错误:
$ cargo run
Compiling borrowing v0.1.0 (file:///projects/borrowing)
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>
并不能完全绕过借用规则:在
compiler 允许这种内部可变性,并且会检查借用规则
而是在 runtime 中。如果你违反了规则,你会收到一个 panic!
,而不是编译器错误。
让我们通过一个实际示例来研究一下,我们可以使用 RefCell<T>
来改变不可变值,看看为什么这很有用。
内部可变性的用例:Mock 对象
有时在测试过程中,程序员会使用一种类型代替另一种类型,以便观察特定行为并断言它已正确实现。此占位符类型称为 test double。把它想象成电影制作中的“特技替身”,一个人介入并代替演员来表演特定的棘手场景。当我们运行测试时,测试替身可以代替其他类型。Mock 对象是特定类型的测试替身,用于记录测试期间发生的情况,以便您可以断言发生了正确的作。
Rust 没有像其他语言那样有对象,而且 Rust 不像其他一些语言那样在标准库中内置了 mock 对象功能。但是,您绝对可以创建一个与 mock 对象具有相同目的的结构体。
以下是我们将测试的场景:我们将创建一个库,该库根据最大值跟踪值,并根据当前值与最大值的接近程度发送消息。例如,此库可用于跟踪用户允许进行的 API 调用次数的配额。
我们的库将仅提供跟踪值与最大值的接近程度以及消息在什么时间应该是什么的功能。使用我们的库的应用程序将需要提供发送消息的机制:应用程序可以在应用程序中放置消息、发送电子邮件、发送短信或其他内容。库不需要知道该详细信息。它所需要的只是实现我们将提供的名为 Messenger
的 trait 的东西。示例 15-20 显示了库代码:
文件名: src/lib.rs
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!");
}
}
}
示例 15-20:一个库,用于跟踪值与最大值的接近程度,并在值处于特定水平时发出警告
这段代码的一个重要部分是 Messenger
trait 有一个叫做 send
的方法,它接受对 self
和消息文本的不可变引用。这个 trait 是我们的 mock 对象需要实现的接口,这样 mock 就可以像真实对象一样使用。另一个重要的部分是,我们想set_value
在
LimitTracker 的 LimitTracker
中。我们可以更改为 value
参数传入的内容,但是
set_value
不会返回任何内容供我们进行断言。我们希望能够说,如果我们创建一个 LimitTracker
,其中包含实现 Messenger
trait 和 max
的特定值,那么当我们为 value
传递不同的数字时,会告诉 Messenger 发送适当的消息。
我们需要一个 mock 对象,当我们调用 send
时,它不会发送电子邮件或短信,而只会跟踪它被告知发送的消息。我们可以创建 mock 对象的新实例,创建一个使用该 mock 对象的 LimitTracker
,在 LimitTracker
上调用 set_value
方法,然后检查 mock 对象是否具有我们期望的消息。示例 15-21 展示了实现一个 mock 对象来做到这一点的尝试,但 borrow 检查器不允许这样做:
文件名: src/lib.rs
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::*;
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);
}
}
示例 15-21:尝试实现 MockMessenger
这是 Borrow Checker 不允许的
此测试代码定义了一个 MockMessenger
结构体,该结构体具有 sent_messages
字段,其中包含 String
值的 Vec
值,以跟踪它被告知发送的消息。我们还定义了一个关联的函数 new
,以便于创建以空消息列表开头的新 MockMessenger
值。然后,我们为 MockMessenger
实现 Messenger
trait,以便我们可以给出一个
MockMessenger
设置为 LimitTracker
。在 send
方法的定义中,我们将传入的消息作为参数,存储在 MockMessenger
中
sent_messages
列表。
在测试中,我们将测试当 LimitTracker
被告知设置
值
设置为大于 Max
值的 75% 的值。首先,我们创建一个新的 MockMessenger
,它将以一个空的消息列表开始。然后我们创建一个新的 LimitTracker
并给它一个对新
MockMessenger
和最大值
100。set_value
我们在
LimitTracker
的值为 80,即 100 的 75% 以上。然后我们断言 MockMessenger
正在跟踪的消息列表现在应该有一条消息。
但是,此测试存在一个问题,如下所示:
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
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 the data it refers to 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
方法采用对 self
的不可变引用。我们也不能从错误文本中获取建议来使用 &mut self
,因为那样 send
的签名会与 Messenger
trait 定义中的签名不匹配(请随意尝试并查看您收到的错误消息)。
在这种情况下,内部可变性可以提供帮助!我们将存储
sent_messages
RefCell<T>
中,然后 send
方法将能够修改 sent_messages
来存储我们看到的消息。示例 15-22 显示了它是什么样子的:
文件名: src/lib.rs
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() {
// --snip--
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-22:使用 RefCell<T>
改变内部值,而外部值被认为是不可变的
sent_messages
字段现在的类型为 RefCell<Vec<String>>,
而不是
Vec<String>
.在新函数中
,我们创建一个新的 RefCell<Vec<String>>
实例。
对于 send
方法的实现,第一个参数仍然是 self
的不可变借用,它与 trait 定义匹配。我们调用
borrow_mut
self.sent_messages
中的 RefCell<Vec<String>>
以获取对 RefCell<Vec<String>>
内部值的可变引用,即向量。然后,我们可以对 vector 的可变引用调用 push
来跟踪测试期间发送的消息。
我们必须做的最后一个更改是在断言中:要查看内部向量中有多少项目,我们在 RefCell<Vec<String>>
上调用 borrow
来获取对向量的不可变引用。
现在您已经了解了如何使用 RefCell<T>
,让我们深入了解它是如何工作的!
使用 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>
增加其活动不可变借用数的计数。当 A 参考<T>
value 超出范围,则不可变借用的计数将减少 1。只
与编译时借用规则一样,RefCell<T>
允许我们在任何时间点拥有多个不可变借款或一个可变借款。
如果我们试图违反这些规则,而不是像引用那样得到编译器错误,RefCell<T>
的实现将在运行时 panic。示例 15-23 显示了示例 15-22 中 send
实现的修改。我们特意尝试为同一范围创建两个活动的可变借用,以说明 RefCell<T>
阻止我们在运行时执行此作。
文件名: src/lib.rs
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) {
let mut one_borrow = self.sent_messages.borrow_mut();
let mut two_borrow = self.sent_messages.borrow_mut();
one_borrow.push(String::from(message));
two_borrow.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-23:在同一个范围内创建两个可变引用,以查看 RefCell<T>
会 panic
我们为从 borrow_mut
返回的 RefMut<T>
智能指针创建一个变量 one_borrow
。然后我们在变量 two_borrow
中以相同的方式创建另一个可变借款。这会在同一范围内生成两个可变引用,这是不允许的。当我们为我们的库运行测试时,示例 15-23 中的代码将编译而不会出现任何错误,但测试将失败:
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde)
running 1 test
test tests::it_sends_an_over_75_percent_warning_message ... FAILED
failures:
---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at src/lib.rs:60:53:
already borrowed: BorrowMutError
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::it_sends_an_over_75_percent_warning_message
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
请注意,代码出现 panic 并显示消息 already borrowed: BorrowMutError
.这就是 RefCell<T>
在运行时处理违反借用规则的方式。
选择在运行时捕获借用错误,而不是像我们在这里所做的那样在编译时捕获借用错误,这意味着您可能会在开发过程的后期发现代码中的错误:可能直到您的代码部署到生产环境时才会发现错误。此外,由于在运行时而不是编译时跟踪借用,您的代码将产生较小的运行时性能损失。但是,使用 RefCell<T>
可以编写一个模拟对象,该对象可以修改自身以跟踪在仅允许不可变值的上下文中使用它时看到的消息。您可以使用 RefCell<T>
尽管它需要权衡取舍以获得比常规引用更多的功能
提供。
通过组合 rc<T>
和 RefCell<T>
拥有可变数据的多个所有者
使用 RefCell<T>
的一种常用方法是与 Rc<T>
结合使用。回想一下
Rc<T>
允许您拥有某些数据的多个所有者,但它只提供对该数据的不可变访问权限。如果您有一个 Rc<T>
,它包含一个 RefCell<T>
,您可以得到一个可以有多个所有者并且您可以更改的值!
例如,回想一下示例 15-18 中的 cons list 示例,我们在那里使用了
Rc<T>
允许多个列表共享另一个列表的所有权。因为
Rc<T>
仅包含不可变值,一旦创建它们,我们就无法更改列表中的任何值。让我们添加 RefCell<T>
以获得
更改列表中的值。示例 15-24 显示,通过使用
RefCell<T>
在 Cons
定义中,我们可以修改存储在所有列表中的值:
文件名: src/main.rs
#[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:?}"); }
示例 15-24:使用 Rc<RefCell<i32>>
创建一个
我们可以 mutate 的列表
我们创建一个值,它是 Rc<RefCell<i32>>
的实例,并将其存储在名为 value
的变量中,以便我们稍后可以直接访问它。然后我们创建一个
在
a
中列出具有 Value 的 Cons
变体。
我们需要克隆
value
的 API 中,因此 a
和 value
都拥有内部 5
value 的所有权,而不是将所有权从 value
转移到 a
或借用
值
。
我们将列表 a
包装在 Rc<T>
中,这样当我们创建列表 b
和 c
时,它们都可以引用 a
,这就是我们在示例 15-18 中所做的。
在 a
、b
和 c
中创建列表后,我们希望将 value
中的 value 加 10。我们通过在 value
上调用 borrow_mut
来实现这一点,它使用
我们在第 5 章中讨论的自动取消引用功能(参见
“->
运算符在哪里?”)将 Rc<T>
取消引用到内部 RefCell<T>
值。borrow_mut
method 返回一个 RefMut<T>
智能指针,我们对它使用 dereference 运算符并更改内部值。
当我们打印 a
、b
和 c
时,我们可以看到它们的修改值都是 15 而不是 5:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.63s
Running `target/debug/cons-list`
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))
这个技术很巧妙!通过使用 RefCell<T>
,我们得到了一个外部不可变的 List
值。但是我们可以使用 RefCell<T>
上提供对其内部可变性的访问的方法,以便我们可以在需要时修改我们的数据。借用规则的运行时检查可以保护我们免受数据竞争的影响,有时值得以速度换取数据结构的这种灵活性。请注意,RefCell<T>
不适用于多线程代码!
Mutex<T>
是 RefCell<T>
的线程安全版本,我们将讨论
Mutex<T>
在第 16 章中。