RefCell<T>
与内部可变性模式
RefCell<T>
and the Interior Mutability Pattern
内部可变性,interior mutability 属于 Rust 中的一种设计模式,他实现了即使在有着到数据的一些不可变引用之下,对数据加以改变;一般情况下,这样的行为是借用规则所不允许的。为了改变数据,这种模式便运用了数据结构内部的一些 unsafe
代码,来改变了 Rust 监管可变性与借用的一些一般规则。这些不安全代码向编译器表明,咱们自己在手动检查那些规则,而非依赖于编译器为咱们检查那些规则;在第 19 章将进一步讨论这些不安全代码。
只有当我们可以确保在运行时遵循借用规则时,我们才能使用使用内部可变性模式的类型,即使编译器不能保证这一点。然后将涉及的不安全代码包装在安全的 API 中,并且外部类型仍然是不可变的。
咱们来通过检视遵循内部可变性模式的 RefCell<T>
类型来探讨这个概念。
使用 RefCell<T>
在运行时执行借用规则检查
Enforcing Borrowing Rules at Runtime with RefCell<T>
与 Rc<T>
不同,RefCell<T>
类型表示对其所持有的数据的单一所有权。那么,是什么使 RefCell<T>
与 Box<T>
这样的类型不同呢?回顾咱们在第四章学到的借用规则:
- 在任何给定时间,咱们都可以有着 要么 (而非同时) 一个可变引用,要么任意数量的不可变引用;
- 引用必须始终有效。
对于引用与 Box<T>
,借用规则的不变性,the borrowing rules' invariants, 是在编译时强制执行的。对于 RefCell<T>
,这些不变性则是在运行时强制执行的。对于引用,如果咱们破坏了这些规则,咱们会得到编译器报错。而在 RefCell<T>
中,如果咱们破坏了这些规则,咱们的程序将终止运行。
在编译时检查借用规则的好处是在开发过程中会更早地发现错误,而且对运行时性能没有影响,因为所有分析都是事先完成的。由于这些原因,在大多数情况下,在编译时检查借用规则是最好的选择,这就是为什么这是 Rust 的默认设置。
相反,在运行时检查借用规则的优点是允许某些内存安全的场景,而编译时检查则不会允许这些场景。与 Rust 编译器一样,静态分析,static analysis,本质上是保守的。代码的某些属性无法通过分析代码来检测:最著名的例子是停机问题,the 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>
中的值。
改变不可变值内部的值,就是 内部可变性模式,the interior mutablity pattern。让我们看一下内部可变性有用的一种情况,并检视其如何可行。
内部可变性:对不可变值的可变借用
Interior Mutability: A Mutable Borrow to an Immutable Value
借用规则的一种后果是,当咱们有一个不可变的值时,咱们不能以可变方式借用他。比如,下面这段代码就不能编译:
fn main() { let x = 5; let y = &mut x; }
如果咱们试图编译这段代码,咱们会得到以下错误:
$ cargo run
Compiling sp_demos v0.1.0 (/home/lennyp/rust-lang/sp_demos)
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
--> src/main.rs:3:13
|
2 | let x = 5;
| - help: consider changing this to be mutable: `mut x`
3 | let y = &mut x;
| ^^^^^^ cannot borrow as mutable
For more information about this error, try `rustc --explain E0596`.
error: could not compile `sp_demos` due to previous error;
然而,在有些情况下,值在其方法中改变自身,但对其他代码来说却显得不可改变,这将是非常有用的。在该值的方法之外的代码将不能改变该值。使用 RefCell<T>
是获得内部可变性能力的一种方法,但是 RefCell<T>
并没有完全绕过借用规则:编译器中的借用检查器会放行这种内部可变性,而代之以在运行时借用规则得以检查。如果咱们违反了这些规则,咱们会得到一个 pani!
而不是一个编译器报错。
咱们来通过一个其中咱们可以使用 RefCell<T>
改变一个不可变的值的实际示例,看看为什么这很有用。
内部可变性的用例:模拟对象
A Use Case for Interior Mutability: Mock Objects
有时在测试过程中,程序员会使用一个类型来代替另一类型,以便观察特定的行为并断定其实现是正确的。这种占位符类型被称为 测试替身,test double。请从电影制作中的“特技替身,stunt double”的角度来考虑他,某人代替一名演员来完成特别棘手的一场戏。当咱们运行测试时,测试替身代表其他类型。模拟对象,mock objects 是特定类型的测试替身,他记录了测试过程中发生的事情,因此咱们可以断言发生了正确的动作。
Rust 没有像其他语言那样拥有对象,Rust 也没有像其他一些语言那样在标准库中内置模拟对象功能。但是,咱们绝对可以创建一个与模拟对象具有相同用途的结构。
下面是我们要测试的情景:我们将创建一个库,跟踪某个数值与最大值的关系,并根据当前数值与最大值的接近程度发送消息。例如,这个库可以用来跟踪用户允许调用的 API 数量配额。
这个库将提供跟踪某个值接近最大值的程度,及在什么时刻发出什么消息的功能。使用这个库的应用,将被期望提供发送消息的机制:应用可以在应用中放置消息、发送电子邮件、发出手机短信或其他东西。库不需要知道这个细节。他所需的只是实现了咱们将提供的名为 Messenger
特质的东西。以下清单 15-20 给出库的代码:
文件名:src/lib.rs
#![allow(unused)] fn main() { {{#include ../projects/limit_tracker/src/lib.rs::35}} }
清单 15-20:跟踪某个值与最大值接近程度,并在值处于不同水平时发出告警的库
这段代码的一个重要部分是 Messenger
特质有个叫做 send
的方法,其接收一个不可变 self
的引用和消息文本。这个特质是咱们模拟对象需要实现的接口,这样模拟对象就可以和真实对象一样被使用。另一个重要的部分是,我们要测试 LimitTracker
上 set_value
方法的行为。我们可以改变我们传入的 value
参数,但 set_value
并没有返回任何东西让我们做断言。我们希望能够表达出,若咱们用实现了 Messenger
特质的东西,与 max
的一个特定值创建了一个 LimitTracker
,当我们为 value
传递不同的数字时,messenger
就会被告知要发送相应的消息。
注:the interface, 借鉴了 Java 语言的叫法,参见 使用接口来拯救!。而这种内部可变性模式用到的数据结构,则类似于 Java 中的内部类。
我们需要一个模拟对象,他不会在我们调用 send
时发送电子邮件或文本消息,而只会记录他被告知要发送的消息。我们可以创建模拟对象的一个新实例,创建一个使用该模拟对象的 LimitTracker
实例,调用 LimitTracker
实例的 set_value
方法,然后检查该模拟对象是否有我们期望的消息。清单 15-21 给出了一个实现模拟对象的尝试,来就这样做,但借用检查器不允许这样做:
文件名:src/lib.rs
#![allow(unused)] fn main() { #[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
这段测试代码定义了一个 MockMessenger
结构体,他有一个 send_messages
字段,里面有一个 String
值的 Vec
,用来记录他被告知要发送的消息。我们还定义了一个关联函数 new
,以方便创建新的 MockMessenger
值,该值以一个空的消息列表开始。然后我们为 MockMessenger
实现了 Messenger
特质,这样我们就可以给 LimitTracker
一个 MockMessenger
。在 send
方法的定义中,我们将传入的消息作为参数,并将其存储在 MockMessenger
的 send_messages
列表中。
在测试中,我们正在测试当 LimitTracker
被告知将 value
设置为超过最大值的 75% 时会发生什么。首先,我们创建一个新的 MockMessenger
,他将以一个空的消息列表开始。然后我们创建一个新的 LimitTracker
并为其提供对新 MockMessenger
的引用和最大值 100
。我们在 LimitTracker
上用一个大于 75% 的值 80
调用 set_value
方法。然后我们断言 MockMessenger
正在跟踪的消息列表现在应有一条消息。
但是,此测试存在一个问题,如下所示:
$ cargo test
Compiling limit_tracker v0.1.0 (/home/lennyp/rust-lang/limit_tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
--> src/lib.rs:58:13
|
2 | fn send(&self, msg: &str);
| ----- help: consider changing that to be a mutable reference: `&mut self`
...
58 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit_tracker` due to previous error
warning: build failed, waiting for other jobs to finish...
我们不能修改 MockMessenger
来记录消息,因为 send
方法需要一个对 self
的不可变的引用。我们也不能采纳错误文本中的建议,使用 &mut self
来代替,因为那样的话,send
的签名就无法与 Messenger
特质定义中的签名相匹配(请随意尝试,看看咱们会得到什么样的报错消息)。
这种情况下,内部可变性可以起到帮助作用!我们将把 send_messages
存储在一个 RefCell<T>
中,然后 send
方法将能够修改 send_messages
来存储我们所看到的信息。清单 15-22 显示了这是什么样子:
文件名:src/lib.rs
#![allow(unused)] fn main() { {{#include ../projects/limit_tracker/src/lib.rs:37:}} }
清单 15-22:使用 RefCell<T>
来改变内层值,而外部值被认为是不可变的
sent_messages
字段现在的类型是 RefCell<Vec<String>
,而不是 Vec<String>
。在 new
函数中,我们围绕空向量创建一个新的 RefCell<Vec<String>
实例。
对于 send
方法的实现,第一个参数仍然是 self
的不可变借用,这与特质定义相匹配。我们对 self.send_messages
中的 RefCell<Vec<String>
调用 borrow_mut
,以获得 RefCell<Vec<String>
中值的可变引用,也就是那个矢量。然后,我们可以对该矢量的可变引用调用 push
,以记录测试期间发送的消息。
我们必须做的最后一个更改是在断言中:为了查看内层矢量中有多少个条目,我们在 RefCell<Vec<String>>
上调用 borrow
以获得对该矢量的不可变引用。
现在咱们已经看到了如何使用 RefCell<T>
,咱们来深入了解其工作原理!
使用 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>
在运行时阻止咱们这样做。
文件名:src/lib.rs
#![allow(unused)] fn main() { impl Messenger for MockMessenger { fn send(&self, message: &str) { let mut borrow_one = self.sent_messages.borrow_mut(); let mut borrow_two = self.sent_messages.borrow_mut(); borrow_one.push(String::from(message)); borrow_two.push(String::from(message)); } } }
清单 15-23:在同一作用域中创建两个可变引用,以发现 RefCell<T>
会终止运行
我们为从 borrow_mut
返回的 RefMut<T>
智能指针创建了一个变量 one_borrow
。然后我们以同样的方式在变量 two_borrow
中创建了另一个可变的借用。这就在同一作用域中产生了两个可变引用,这是不允许的。当我们运行咱们库的测试时,清单 15-23 中的代码将被不带任何报错地编译,但测试将失败:
$ cargo test
Compiling limit_tracker v0.1.0 (/home/peng/rust-lang/limit_tracker)
Finished test [unoptimized + debuginfo] target(s) in 0.46s
Running unittests src/lib.rs (target/debug/deps/limit_tracker-98d6159d1b15eb72)
running 1 test
test tests::it_sends_an_over_75_percent_waring_message ... FAILED
failures:
---- tests::it_sends_an_over_75_percent_waring_message stdout ----
thread 'tests::it_sends_an_over_75_percent_waring_message' panicked at 'already borrowed: BorrowMutError', src/lib.rs:60:53
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::it_sends_an_over_75_percent_waring_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>
,实现可变数据的多个所有者
Having Multiple Owners of Mutable Data by Combining Rc<T>
and RefCell<T>
使用 RefCell<T>
的一种常见方式是与 Rc<T>
结合使用。回顾一下,Rc<T>
实现了某个数据的多个所有者,但只提供对数据的不可变访问。如果咱们有一个持有 RefCell<T>
的 Rc<T>
,咱们可以得到一个可以有着多个所有者,且 咱们可以改变的值。
比如,回顾清单 15-18 中的构造列表示例,咱们使用 Rc<T>
来实现多个列表共用另一列表所有权。由于 Rc<T>
只保存不可变值,因此一旦咱们创建出列表中的任何值,咱们就再也不能改变他们。咱们来加入 RefCell<T>
,以获得修改列表中值的能力。下面清单 15-24 显示,通过在 Cons
定义中使用 RefCell<T>
,咱们可以修改所有列表中存储的值:
文件名:src/main.rs
#![allow(unused)] fn main() { {{#include ../projects/cons_list_demo/src/main.rs}} }
清单 15-24:使用 Rc<RefCell<i32>>
创建一个咱们可改变的 List
我们创建了一个值,他是 Rc<RefCell<i32>>
的一个实例,并将其存储在一个名为 value
的变量中,以便我们稍后可以直接访问。然后我们以持有 value
的一个 Cons
变种,在 a
中创建了一个 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
来实现这一目的,他使用了我们在第 5
章中讨论过的自动解引用功能,the automatic dereferencing feature,(参见 ->
操作符去哪儿了? 小节),将 Rc<T>
解引用到内部的 RefCell<T>
值。borrow_mut
方法返回一个 RefMut<T>
灵巧指针,我们对其使用解引用操作符,并改变内部值。
在打印 a
、b
与 c
时,就可以看到他们都有了修改后的值 15
而非 5
:
$ cargo run lennyp@vm-manjaro
Compiling cons_list_demo v0.1.0 (/home/lennyp/rust-lang/cons_list_demo)
Finished dev [unoptimized + debuginfo] target(s) in 0.17s
Running `target/debug/cons_list_demo`
之后的 a = Cons(RefCell { value: 15 }, Nil)
之后的 b = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
之后的 c = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))
这个技巧非常整洁! 通过使用 RefCell<T>
,我们有一个对外不可变的 List
值。但是我们可以使用 RefCell<T>
上提供对其内部可变性访问的方法,这样我们就可以在需要的时候修改我们的数据。借用规则的运行时检查可以保护我们不受数据竞赛的影响,有时值得用一点速度来换取我们数据结构中的这种灵活性。请注意,RefCell<T>
对多线程代码不起作用! Mutex<T>
是 RefCell<T>
的线程安全版本,我们将在第 16 章讨论 Mutex<T>
。