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