RefCell<T>
和 Interior Mutability 模式
内部可变性是 Rust 中的一种设计模式,允许你进行 mut
data 即使存在对该数据的不可变引用;通常,这个
作 不允许借用规则。为了改变数据,该模式使用unsafe
代码来改变 Rust 的通常规则
突变和借用。不安全代码向编译器表明我们是
手动检查规则,而不是依赖编译器来检查它们
对我们来说;我们将在第 19 章中更多地讨论不安全代码。
只有在可以的情况下,我们才能使用使用 interior 可变性模式的类型
确保在运行时遵循借用规则,即使
编译器无法保证这一点。这unsafe
然后,所涉及的代码将包装在
safe API 的 API 中,并且外部类型仍然是不可变的。
让我们通过查看RefCell<T>
类型,该类型跟在
内部可变性模式。
在运行时实施借用规则RefCell<T>
与Rc<T>
这RefCell<T>
type 表示对数据的单一所有权
它成立。那么,是什么造就了RefCell<T>
与像Box<T>
?
回想一下你在第 4 章中学到的借用规则:
- 在任何给定时间,您都可以拥有一个(但不能同时拥有)一个可变引用 或任意数量的不可变引用。
- 引用必须始终有效。
使用引用和Box<T>
,借用规则的不变量在
编译时。跟RefCell<T>
,则这些不变量将在运行时强制执行。
对于引用,如果你违反了这些规则,你将得到一个编译器错误。跟RefCell<T>
,如果违反这些规则,程序将 panic 并退出。
在编译时检查借用规则的优点是 error 会在开发过程中更早地捕获,并且不会对 运行时性能,因为所有分析都是事先完成的。对于那些 原因,在编译时检查借用规则是 大多数情况下,这就是为什么这是 Rust 的默认。
相反,在运行时检查借用规则的优点是 然后,允许某些内存安全方案,它们本应是 被编译时检查不允许。静态分析,如 Rust 编译器, 本质上是保守的。代码的某些属性无法通过 分析代码:最著名的例子是 Halting Problem,它是 超出了本书的范围,但这是一个有趣的研究话题。
因为某些分析是不可能的,如果 Rust 编译器无法确定
code 符合所有权规则,它可能会拒绝正确的程序;在
这样,它是保守的。如果 Rust 接受了不正确的程序,则用户
将无法信任 Rust 所做的保证。但是,如果 Rust
拒绝正确的程序,程序员会感到不便,但什么都没有
灾难性可能会发生。这RefCell<T>
type 在你确定你的
代码遵循借用规则,但编译器无法理解和
保证这一点。
似Rc<T>
,RefCell<T>
仅用于单线程方案
如果您尝试在多线程中使用它,则会出现编译时错误
上下文。我们将讨论如何获取RefCell<T>
在
multithreaded program 的命令。
以下是选择的原因Box<T>
,Rc<T>
或RefCell<T>
:
Rc<T>
支持同一数据的多个所有者;Box<T>
和RefCell<T>
拥有单一所有者。Box<T>
允许在编译时检查不可变或可变借用;Rc<T>
只允许在编译时检查不可变的借款;RefCell<T>
允许 在运行时检查 Immutable 或 Mutable Borrows。- 因为
RefCell<T>
允许在运行时检查可变借用,您可以 mutate 中的 valueRefCell<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
但是,在某些情况下,值发生 mutate 会很有用
本身,但对其他代码似乎不可变。代码
value 的方法将无法改变该值。用RefCell<T>
是
一种方法是获得具有内部可变性的能力,但RefCell<T>
并不能完全绕过借用规则:在
compiler 允许这种内部可变性,并且会检查借用规则
而是在 runtime 中。如果您违反规则,您将收到panic!
而不是
编译器错误。
让我们通过一个实际示例来使用RefCell<T>
以 mutate
一个不可变的值,并了解为什么它很有用。
内部可变性的用例:Mock 对象
有时在测试过程中,程序员会使用一种类型来代替另一种类型。 为了观察特定行为并断言它被正确实现。 此占位符类型称为 test double。从 a 电影制作中的“特技替身”,一个人介入并替代 actor 来做一个特别棘手的场景。测试替身替代其他类型 当我们运行测试时。Mock 对象是特定类型的测试替身 记录测试期间发生的情况,以便您可以断言正确的 行动发生了。
Rust 没有像其他语言那样有对象, 并且 Rust 没有内置到标准库中的 mock 对象功能 就像其他一些语言一样。但是,您绝对可以创建一个结构体 将用于与 mock 对象相同的目的。
以下是我们将测试的场景:我们将创建一个跟踪值的库 根据最大值发送消息,并根据接近最大值的程度发送消息 value 的 current value 为这个库可用于跟踪 例如,用户允许进行的 API 调用次数的配额。
我们的库将仅提供跟踪与
maximum a value is 以及消息在什么时间应该是什么。应用
将需要提供发送
messages:应用程序可以在应用程序中放置一条消息,发送一个
电子邮件、发送短信或其他内容。库不需要知道
那个细节。它所需要的只是实现我们将提供的 trait 的东西
叫Messenger
.示例 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:一个用于跟踪 value 的值为最大值,并在该值处于特定级别时发出警告
此代码的一个重要部分是Messenger
trait 只有一个方法
叫send
它接受对self
和
消息。这个 trait 是我们的 mock 对象需要实现的接口,以便
mock 可以像真实对象一样使用。另一个重要部分
是我们希望测试set_value
方法上的LimitTracker
.我们可以更改传入的value
parameter 的set_value
不会返回任何内容供我们进行断言。我们希望成为
可以说,如果我们创建一个LimitTracker
与实现
这Messenger
trait 和max
,当我们传递不同的
的 numbersvalue
,则指示 Messenger 发送相应的消息。
我们需要一个 mock 对象,而不是发送电子邮件或短信
叫send
,将只跟踪它被告知发送的消息。我们可以
创建 mock 对象的新实例,创建一个LimitTracker
,它使用
mock 对象中,调用set_value
method 开启LimitTracker
,然后检查
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
字段,其中Vec
之String
值来跟踪它被告知的消息
以发送。我们还定义了一个关联的函数new
为了方便
新建MockMessenger
以空消息列表开头的值。我们
然后实现Messenger
的 traitMockMessenger
所以我们可以给出一个MockMessenger
更改为LimitTracker
.在send
方法,我们
将传入的消息作为参数存储在MockMessenger
列表sent_messages
.
在测试中,我们将测试当LimitTracker
被告知设置value
更改为超过 75% 的max
价值。首先,我们
新建MockMessenger
,它将以空消息列表开头。
然后我们创建一个新的LimitTracker
并为其提供对新MockMessenger
以及max
值为 100。我们调用set_value
方法上的LimitTracker
值为 80,即 100 的 75% 以上。然后
我们断言,MockMessenger
正在跟踪
of 现在应该包含一条消息。
但是,此测试存在一个问题,如下所示:
$ 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
method 接受对self
.我们也不能接受
错误文本中要使用的建议&mut self
相反,因为这样
签名send
与Messenger
特性
定义(请随意尝试并查看您收到的错误信息)。
在这种情况下,内部可变性可以提供帮助!我们将存储sent_messages
在RefCell<T>
,然后是send
method 将为
能够修改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>
要更改 Inner
value 的值,而外部值被认为是不可变的
这sent_messages
field 现在为RefCell<Vec<String>>
而不是Vec<String>
.在new
函数,我们会创建一个新的RefCell<Vec<String>>
实例。
为了实现send
方法,则第一个参数仍然是
的 immutable borrowself
,这与特征定义匹配。我们调用borrow_mut
在RefCell<Vec<String>>
在self.sent_messages
要获取
mutable 引用添加到RefCell<Vec<String>>
,即
向量。然后我们可以调用push
在对 vector 的可变引用上保留
track 测试期间发送的消息。
我们要做的最后一个更改是在 assert 中:查看有多少项是
在内部向量中,我们调用borrow
在RefCell<Vec<String>>
要获取
对 vector 的不可变引用。
现在,您已经了解了如何使用RefCell<T>
,让我们深入了解它是如何工作的!
在运行时跟踪借款RefCell<T>
在创建不可变和可变引用时,我们使用 和&
&mut
语法。跟RefCell<T>
,我们使用borrow
和borrow_mut
方法,这些 API 是属于RefCell<T>
.这borrow
method 返回智能指针类型Ref<T>
和borrow_mut
返回智能指针类型RefMut<T>
.两种类型都实现Deref
,所以我们
可以将它们视为常规引用。
这RefCell<T>
跟踪数量Ref<T>
和RefMut<T>
聪明
指针当前处于活动状态。每次我们调用borrow
这RefCell<T>
增加其活动不可变借用数的计数。当Ref<T>
value 超出范围,则不可变借用的计数将减少 1。只
就像编译时借用规则一样,RefCell<T>
让我们有许多不可变的
borrows 或一个可变 borrow。
如果我们试图违反这些规则,而不是像
将与参考一起,实现RefCell<T>
会惊慌
运行。示例 15-23 显示了对send
在
示例 15-22.我们特意尝试创建两个 active 的可变借用
对于相同的范围,以说明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>
会恐慌
我们创建一个变量one_borrow
对于RefMut<T>
返回智能指针
从borrow_mut
.然后我们以相同的方式在
变量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`
请注意,代码 paniced 并显示消息already borrowed: BorrowMutError
.就是这样RefCell<T>
处理借用违规
规则。
选择在运行时而不是编译时捕获借用错误,因为
我们在这里完成了,这意味着您稍后可能会在代码中发现错误
在开发过程中:可能直到您的代码部署到
生产。此外,您的代码将产生较小的运行时性能损失,因为
这是在运行时而不是编译时跟踪借用的结果。
但是,使用RefCell<T>
可以编写一个 mock 对象,该对象可以
修改自身以跟踪您在使用它时看到的消息
在只允许不可变值的上下文中。您可以使用RefCell<T>
尽管它需要权衡取舍以获得比常规引用更多的功能
提供。
通过组合拥有可变数据的多个所有者Rc<T>
和RefCell<T>
一种常见的使用RefCell<T>
与Rc<T>
.回想一下Rc<T>
允许您拥有某些数据的多个所有者,但它只提供 Immutable
访问该数据。如果你有Rc<T>
,它持有一个RefCell<T>
您可以
获取一个可以具有多个所有者并且可以更改的值!
例如,回想一下示例 15-18 中的 cons list 示例,我们在那里使用了Rc<T>
以允许多个列表共享另一个列表的所有权。因为Rc<T>
只保存不可变值,我们不能更改
list 创建它们后。让我们添加RefCell<T>
要获得
更改列表中的值。示例 15-24 显示,通过使用RefCell<T>
在Cons
定义,我们可以修改存储在 ALL
名单:
文件名: 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>>
要创建一个List
我们可以变异
我们创建一个值,它是Rc<RefCell<i32>>
并将其存储在
名为value
这样我们就可以稍后直接访问它了。然后我们创建一个List
在a
替换为Cons
变体,该value
.我们需要克隆value
所以两者都a
和value
拥有内部5
value 而不是
而不是转移所有权value
自a
或具有a
借款自value
.
我们包装列表a
在Rc<T>
因此,当我们创建列表时b
和c
他们
都可以指a
,这就是我们在示例 15-18 中所做的。
在 中创建列表后a
,b
和c
,我们想将 10 添加到
值value
.我们通过调用borrow_mut
上value
,它使用
我们在第 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 章中。
本文档由官方文档翻译而来,如有差异请以官方英文文档(https://doc.rust-lang.org/)为准