RefCell<T>
和内部可变性模式
内部可变性 是 Rust 中的一种设计模式,它允许你在有不可变引用指向数据的情况下仍然可以修改数据;通常,这种操作是被借用规则禁止的。为了修改数据,该模式在数据结构内部使用 unsafe
代码来绕过 Rust 通常的关于修改和借用的规则。不安全代码向编译器表明我们正在手动检查规则,而不是依赖编译器为我们检查规则;我们将在第 20 章中更详细地讨论不安全代码。
我们只能在确保运行时将遵循借用规则的情况下使用使用内部可变性模式的类型,即使编译器无法保证这一点。unsafe
代码随后会被包装在一个安全的 API 中,而外部类型仍然是不可变的。
让我们通过查看遵循内部可变性模式的 RefCell<T>
类型来探讨这个概念。
在运行时使用RefCell<T>
强制借用规则
Unlike Rc<T>
,RefCell<T>
类型表示对其持有的数据具有单一所有权。那么,RefCell<T>
与 Box<T>
这样的类型有什么不同呢?回想你在第 4 章中学到的借用规则:
- 在任何时候,你可以有 要么(但不能同时拥有两者)一个可变引用 或任意数量的不可变引用。
- 引用必须始终有效。
使用引用和Box<T>
,借用规则的不变性在编译时被强制执行。使用RefCell<T>
,这些不变性在运行时被强制执行。使用引用时,如果你违反了这些规则,你会得到一个编译器错误。使用RefCell<T>
时,如果你违反了这些规则,你的程序将崩溃并退出。
在编译时检查借用规则的优点是在开发过程中可以更早地捕获错误,并且由于所有分析都在事先完成,因此对运行时性能没有影响。基于这些原因,在大多数情况下,编译时检查借用规则是最佳选择,这也是为什么这是 Rust 的默认设置。
在运行时而不是编译时检查借用规则的优势在于,某些内存安全的场景得以允许,而这些场景在编译时检查中会被禁止。静态分析,如 Rust 编译器,本质上是保守的。有些代码的属性是无法通过分析代码来检测的:最著名的例子是停机问题,虽然这超出了本书的范围,但这是一个有趣的研究主题。
因为有些分析是不可能的,如果 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>
内的值。
在不可变值内部改变值是内部可变性模式。让我们来看一个内部可变性有用的情况,并 examine 如何实现。
内部可变性:对不可变值的可变借用
借用规则的一个后果是,当你有一个不可变的值时,你不能借用它为可变的。例如,这段代码无法编译:
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>
并没有完全绕过借用规则:编译器中的借用检查器允许这种内部可变性,而借用规则是在运行时检查的。如果你违反了规则,你将得到一个panic!
而不是编译器错误。
让我们通过一个实际的例子来使用 RefCell<T>
来修改不可变值,并看看为什么这很有用。
A Use Case for Interior Mutability: Mock Objects
有时在测试期间,程序员会使用一种类型代替另一种类型,以便观察特定行为并断言其已正确实现。这种占位符类型称为测试替身。可以将其理解为电影制作中的“替身”,即某人介入并代替演员完成特定的复杂场景。测试替身在我们运行测试时代表其他类型。模拟对象是特定类型的测试替身,它们记录测试期间发生的情况,以便您可以断言正确的操作已发生。
Rust 没有以与其他语言相同的方式拥有对象,且 Rust 的标准库中没有像某些其他语言那样内置模拟对象功能。然而,你绝对可以创建一个结构体来实现与模拟对象相同的目的。
这里是我们将测试的场景:我们将创建一个库,该库跟踪一个值相对于最大值,并根据当前值接近最大值的程度发送消息。这个库可以用于跟踪用户被允许调用的API数量的配额,例如。
我们的库将只提供跟踪一个值离最大值有多近以及在什么时间应该有什么消息的功能。使用我们库的应用程序将被期望提供发送消息的机制:应用程序可以将消息放入应用程序中,发送电子邮件,发送短信,或其他方式。库不需要知道这些细节。它只需要一个实现了我们提供的名为Messenger
的特质的东西。清单15-20显示了库代码:
这段代码的一个重要部分是 Messenger
特性有一个名为 send
的方法,该方法接受一个对 self
的不可变引用和消息的文本。这个特性是我们模拟对象需要实现的接口,以便模拟对象可以像真实对象一样使用。另一个重要部分是我们想要测试 LimitTracker
上 set_value
方法的行为。我们可以改变传递给 value
参数的值,但 set_value
不返回任何内容供我们进行断言。我们希望能够说,如果我们使用实现了 Messenger
特性的对象和特定的 max
值创建一个 LimitTracker
,当我们传递不同的 value
数值时,信使会被告知发送适当的消息。
我们需要一个模拟对象,当我们在调用send
时,它不会发送电子邮件或短信,而只会记录它被告知要发送的消息。我们可以创建一个模拟对象的新实例,创建一个使用模拟对象的LimitTracker
,在LimitTracker
上调用set_value
方法,然后检查模拟对象是否具有我们期望的消息。列表15-21展示了一次尝试实现这样一个模拟对象的尝试,但借用检查器不允许这样做:
这个测试代码定义了一个 MockMessenger
结构体,它有一个 sent_messages
字段,包含一个 Vec
的 String
值,用于记录它被告知要发送的消息。我们还定义了一个关联函数 new
,以便于创建新的 MockMessenger
值,这些值从一个空的消息列表开始。然后我们为 MockMessenger
实现了 Messenger
特性,这样我们就可以将一个 MockMessenger
给 LimitTracker
。在 send
方法的定义中,我们将作为参数传递的消息存储在 MockMessenger
的 sent_messages
列表中。
在测试中,我们测试当 LimitTracker
被告知将 value
设置为超过 max
值的 75% 时会发生什么。首先,我们创建一个新的 MockMessenger
,它将从一个空的消息列表开始。然后我们创建一个新的 LimitTracker
并给它一个指向新的 MockMessenger
的引用和一个 max
值 100。我们调用 LimitTracker
的 set_value
方法,将值设置为 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
特性定义中的签名不匹配(可以尝试看看会得到什么错误信息)。
这是一个内部可变性可以提供帮助的情况!我们将 sent_messages
存储在 RefCell<T>
中,然后 send
方法将能够修改 sent_messages
以存储我们看到的消息。清单 15-22 显示了这看起来像什么:
sent_messages
字段现在是 RefCell<Vec<String>>
类型,而不是 Vec<String>
。在 new
函数中,我们在空向量周围创建了一个新的 RefCell<Vec<String>>
实例。
对于 send
方法的实现,第一个参数仍然是 self
的不可变借用,这与特质定义匹配。我们在 self.sent_messages
中的 RefCell<Vec<String>>
上调用 borrow_mut
以获取 RefCell<Vec<String>>
内部值的可变引用,即向量。然后我们可以在向量的可变引用上调用 push
以记录测试期间发送的消息。
我们最后需要做的更改是在断言中:为了查看内部向量中有多少个元素,我们在 RefCell<Vec<String>>
上调用 borrow
以获取向量的不可变引用。
现在你已经看到了如何使用RefCell<T>
,让我们深入探讨它是如何工作的!
Keeping Track of Borrows at Runtime with 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>
的实现将在运行时引发恐慌。列表 15-23 显示了对列表 15-22 中 send
实现的修改。我们故意尝试在同一作用域内创建两个可变借用,以说明 RefCell<T>
会在运行时阻止我们这样做。
我们创建一个变量 one_borrow
用于从 borrow_mut
返回的 RefMut<T>
智能指针。然后我们以相同的方式在变量 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`
请注意,代码以消息 already borrowed: BorrowMutError
恐慌。这就是 RefCell<T>
在运行时处理借用规则违规的方式。
选择在运行时而不是编译时捕获借用错误,如我们在这里所做的,意味着你可能会在开发过程的后期才发现代码中的错误:可能直到你的代码被部署到生产环境。此外,由于在运行时而不是编译时跟踪借用,你的代码会因此承担一小部分运行时性能损失。然而,使用RefCell<T>
可以使你在仅允许不可变值的上下文中编写一个可以修改自身以跟踪其已看到的消息的模拟对象。尽管RefCell<T>
存在权衡,你仍然可以使用它来获得比普通引用更多的功能。
通过结合 Rc<T>
和 RefCell<T>
拥有可变数据的多个所有者
使用 RefCell<T>
的常见方法是与 Rc<T>
结合使用。回想一下,Rc<T>
让你拥有某些数据的多个所有者,但它只提供对该数据的不可变访问。如果你有一个持有 RefCell<T>
的 Rc<T>
,你可以获得一个可以有多个所有者 并且 可以修改的值!
例如,回想一下列表 15-18 中的 cons 列表示例,我们在其中使用了 Rc<T>
以允许多个列表共享另一个列表的所有权。由于 Rc<T>
仅持有不可变值,因此我们一旦创建了列表中的值就无法更改它们。让我们添加 RefCell<T>
以获得更改列表中值的能力。列表 15-24 显示,通过在 Cons
定义中使用 RefCell<T>
,我们可以修改所有列表中存储的值:
我们创建一个 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
中的列表后,我们希望将 10 加到 value
中的值。我们通过调用 value
上的 borrow_mut
方法来实现这一点,该方法使用我们在第 5 章中讨论的自动解引用功能(参见 “->
运算符在哪里?”)将 Rc<T>
解引用到内部的 RefCell<T>
值。borrow_mut
方法返回一个 RefMut<T>
智能指针,我们对其使用解引用运算符并更改内部值。
当我们打印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>
的线程安全版本,我们将在第 16 章讨论 Mutex<T>
。