【Rust自学】15.6. RefCell与内部可变性:“摆脱”安全性限制
题外话,这篇文章一共4050字,是截止到目前为止最长的文章,如果你能坚持读完并理解,那真的很强!
喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
15.6.1. 什么是内部可变性
内部可变性(interior mutability)是Rust的设计模式之一,它允许程序员在只持有不可变引用的前提下对数据进行修改。
通常而言,这样的行为会被借用规则(详见 4.4. 引用与借用)所禁止,但是为了能够改变数据,内部可变性模式在代码的数据结构里使用了unsafe
代码来绕过Rust正常的可变性和借用规则。
不安全代码向编译器表明我们正在手动检查规则,而不是依赖编译器为我们检查规则。不安全代码相关的概念将在以后的文章中涉及。
15.6.2. RefCell<T>
与Rc<T>
不同,RefCell<T>
类型代表了其持有数据的唯一所有权。
为了了解RefCell<T>
与Box<T>
的区别,我们得回顾一下借用规则(详见 4.4. 引用与借用):
- 在任何给定时间,你可以拥有(但不能同时拥有)一个可变引用或任意数量的不可变引用。
- 引用总是保持有效。
PS:给定时间可以理解为给定的作用域内
RefCell<T>
与Box<T>
的区别如下:
类型 | 检查阶段 | 规则违背后果 |
---|---|---|
Box<T> | 编译阶段检查借用规则 | 编译时报错 |
RefCell<T> | 运行时检查借用规则 | 触发 panic |
借用规则在不同阶段进行检查有不同的特点:
-
编译阶段:
- 尽早暴露问题
- 没有任何运行时的开销
- 是大多数场景的最佳选择
- 是Rust的默认行为
-
运行时:
- 问题暴露延后,甚至到生产环境
- 因借用计数产生些许性能损失
- 实现某些特定的内存安全场景(比如在不可变环境中修改自身数据)
该在什么时候使用`RefCell
Rust编译器在编译阶段会检查所有的代码,其中大部分代码它都能够分析明白,如果没有问题就通过编译,如果有问题就报错。
Rust编译器是非常保守的,某些代码并不能在编译阶段就能分析明白,针对这类无法在编译阶段完成分析的代码Rust会直接拒绝掉,哪怕这些代码本质上没有任何问题。
Rust这么保守是为了保证程序的安全性。虽然拒绝掉某些本身没有问题的代码会对开发者造成不便,但是至少不会产生任何灾难性的后果。
针对这些编译器无法分析的代码,如果开发者能够保证这段代码满足借用规则,那么就可以使用RefCell<T>
。
与RefCell<T>
类似,Rc<T>
只适用于单线程场景。
15.6.3. 如何在Box<T>
、Rc<T>
和RefCell<T>
中进行选择
根据下表列出的三者的特性就可以进行选择:
特性 | Box<T> | Rc<T> | RefCell<T> |
---|---|---|---|
同一数据的所有者 | 一个 | 多个 | 一个 |
可变性、借用检查 | 可变、不可变借用(编译时检查) | 不可变借用(编译时检查) | 可变、不可变借用(运行时检查) |
额外说一句,由于RefCell<T>
在运行时才会被检查,所以即使RefCell<T>
本身是不可变的,但我们仍然可以修改里面储存的值。
15.6.4. 内部可变形:可变的借用一个不可变的值
这个小标题有一点绕,意思是对一个没有声明为mut
的类型使用&mut
引用。看个例子就明白了:
fn main() {
let x = 5;
let y = &mut x;
}
借用规则的一个推论是,当你有一个不可变的值时,你就不能可变地借用它。所以这么写会报错:
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>
并没有完全地绕开借用规则,编译阶段的检查虽然能够通过,但是在运行阶段如果违反了借用规则就会造成程序恐慌。
下面看一个例子(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!");
}
}
}
这个例子的逻辑并不重要,看一下它的写法:
-
程序开头定义了
Messenger
trait,里面有send
方法的签名:接收不可变引用&self
和一个字符串切片类型&str
的形参msg
作为参数。 -
下面定义了一个结构体叫
LimitTracker
,它是一个泛型类型,生命周期为'a
,泛型参数为T
,要求T
的生命周期为'a
并实现Messenger
这个在程序开头定义的trait。LimitTracker
里面有三个字段:messenger
:类型为&str
字符串切片类型,生命周期为'a
value
:类型为usize
max
:类型为usize
-
往下看,通过
impl
块在LimitTracker
上写了关联函数new
,其参数是类型为泛型引用&T
的形参messenger
和类型为usize
的形参max
,返回值是LimitTracker
类型。这个函数用于创建LimitTracker
实例,这个实例:messenger
字段是形参menssenger
的值value
字段值为0max
字段值为形参max
的值
-
LimitTracker
还有一个方法叫做set_value
,其第一个参数是self
的可变引用&mut self
,第二个参数是value
,类型为usize
。
方法内部的代码逻辑很简单。把self
的value
字段值和参数value
的值相除(还要转换成f64
避免丢失精度)得到一个百分比,存储在percentage_of_max
内。根据percentage_of_max
的大小使用Messenger
trait下的send
方法发送不同的警告。
使用测试替代(test double)进行测试
这里有一个问题,如果我们要对这个set_value
方法进行测试,就需要这个方法得输出些什么东西以供断言。但是set_value
方法实际上并没有返回任何的值,所以说它不会提供任何的结果来进行断言。
我们要测试的是当某一个实现了Messenger
trait的值和一个max
值来创建LimitTracker
实例时,传入不同的value
就能够触发Messenger
发送不同的消息。
为了解决这个问题,这里要介绍test double,它的中文叫测试替代,是一个通用的变成概念,代表了测试工作中被用作其他类型的替代品。test double中有一个特定的类型,叫模拟对象(Mock Object),它会承担记录测试过程中的工作。我们就可以利用这些记录来断言这个测试工作运行是否正确。
Rust里没有类似的概念,在标准库里也没有模拟对象(Mock Object),但是我们可以自定义一个结构体来实现和Mock Object相同的功能。
接着上文的代码来写:
#[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);
}
}
-
在测试模块的最开头先声明了
MockMessenger
结构体,里面有1个字段sent_message
,表示发送的消息,其类型是Vec<String>
-
MockMessenger
在下文又通过impl
块创建了了new
函数用于创建MockMessenger
的实例,实例的sent_messages
字段的值是一个空的Vector
。 -
后面又为
MockMessenger
结构体实现了整个代码一开头的Messenger
trait。实现了这个trait之后MockMessenger
就可以用来创建LimitTracker
(因为LimitTracker
要求泛型类型实现Messenger
trait)。
使用send
方法时这个消息会存储在MockMessenger
下字段sent_message
这个Vector
里。 -
最后是
it_sends_an_over_75_percent_warning_message
这个测试函数,它测试的是超过75%的这部分。
首先创建了MockMessenger
的实例叫mock_messenger
,然后创建了一个LimitTracker
的实例叫limit_tracker
,接着在LimitTracker
的实例上(就是limit_tracker
)调用。
最后通过mock_messenger
下sent_message
这个Vector
里元素的数量来断言。
此时的代码逻辑有问题,但是运行会报错:
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
实现Messenger
trait时定义send
方法的过程:
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.push(String::from(message));
}
}
无法修改MockMessenger
来跟踪消息,因为send
方法的函数签名参数是对self
不可变引用。我们也无法使用&mut self
来代替,因为这样send
的签名将与Messenger
trait定义中的签名&self
不匹配。
针对这种需要内部可变性的情况,就可以使用RefCell<T>
,只需要把MockMessenger
的sent_messages
字段用RefCell<T>
再包装一下即可:
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
由于RefCell<T>
不在预导入模块中,所以在使用它之前得先把它引入当前作用域
use std::cell::RefCell;
这样改了之后使用了sent_messages
字段的代码都需要使用RefCell<T>
再包装一下:
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
RefCell
到底是怎么用的呢?其实就是用RefCell
创建的数据,可以用borrow_mut
方法来修改,对实参调用borrow_mut
方法即可获得一个可变引用,所以为MockMessenger
实现Messenger
trait时定义send
方法就可以使用borrow_mut
:
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
}
}
这样即使send
的参数是不可变引用,在函数体里也可以通过borrow_mut
来修改其值。
最后把测试函数的断言部分改一下:
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);
}
对mock_messenger
使用borrow
函数即可获取对该变量的不可变引用用于断言。
这时候运行就没有问题了,整体代码如下:
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() {
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.6.5. 使用RefCell<T>
在运行时记录借用信息
实际上,上文所使用的borrow_mut
和borrow
方法相当于提供给用户的两个安全接口:
borrow
:返回智能指针Ref<T>
,它实现了Deref
traitborrow_mut
:返回智能指针RefMut<T>
,实现了Deref
trait
RefCell<T>
会记录当前存在多少活跃的Ref<T>
和RefMut<T>
:
- 每次调用
borrow
:不可变借用计数加1。
任何一个Ref<T>
的值离开作用域被释放:不可变借用计数减1 - 每次调用
borrow_mut
:可变借用计数加1
任何一个RefMut<T>
的值离开作用域被释放:可变借用计数减1
与编译时借用规则(详见 4.4. 引用与借用)一样, RefCell<T>
允许我们在任何时间点拥有许多不可变借用或一个可变借用。
如果我们尝试违反这些规则, RefCell<T>
的实现将在运行时出现恐慌(因为RefCell<T>
在运行时才会进行借用规则检查)。出现恐慌 already borrowed: BorrowMutError
就是RefCell<T>
在运行时处理违反借用规则的方式。
15.6.6. 将Rc<T>
和RefCell<T>
结合使用的例子
Rc<T>
允许某些数据被多个所有者持有,但它只提供对该数据的不可变访问。如果您有一个包含RefCell<T>
的Rc<T>
,你可以获得一个可以拥有多个所有者并且可变的值。
下面看一个将Rc<T>
和RefCell<T>
结合使用来实现多重数据所有权的可变数据:
#[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:?}");
}
还记得上一篇文章的Cons列表示例吗?其中我们使用了Rc<T>
允许多个列表共享另一个列表的所有权。因为Rc<T>
仅保存不可变值,一旦创建了列表中的任何值,我们就无法更改它们。通过这篇文章的内容,让我们添加RefCell<T>
以获得更改列表中的值的能力:
- 首先在生命枚举类型
List
时把Cons
关联的i32
类型用RefCell<>
包裹,由于Rust编译器无法确定RefCell<T>
大小,得用Rc<>
包裹在外,其余保持不变 - 记得引入
Rc
和RefCell
到当前作用域 - 下面通过
Rc::new()
和RefCell::new()
来创建实例,a
通过Rc::clone()
来共享value
的值,b
和c
通过Rc::clone()
来共享a
的值(前提是a
被Rc<>
包裹)。 - 最后通过
RefCell<T>
上的borrow_mut
获得value
的可变引用,其类型时&i32
,然后通过解引用符号*
变为i32
来进行加10的操作。
输出:
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))
跟预期一样,没有问题。
15.6.7. 其他可以实现内部可变性的类型
Cell<T>
:通过复制来访问数据Mutex<T>
:用于实现跨线程情况下的内部可变性模5