💻博主现有专栏:
C51单片机(STC89C516),c语言,c++,离散数学,算法设计与分析,数据结构,Python,Java基础,MySQL,linux,基于HTML5的网页设计及应用,Rust(官方文档重点总结),jQuery,前端vue.js,Javaweb开发,Python机器学习等
🥏主页链接:
目录
内部可变性(Interior mutability)是 Rust 中的一个设计模式,它允许你即使在有不可变引用时也可以改变数据,这通常是借用规则所不允许的。为了改变数据,该模式在数据结构中使用
unsafe
代码来模糊 Rust 通常的可变性和借用规则。不安全代码表明我们在手动检查这些规则而不是让编译器替我们检查。第十九章会更详细地介绍不安全代码。当可以确保代码在运行时会遵守借用规则,即使编译器不能保证的情况,可以选择使用那些运用内部可变性模式的类型。所涉及的
unsafe
代码将被封装进安全的 API 中,而外部类型仍然是不可变的。
🎯通过RefCell<T>在运行时检查借用规则
不同于
Rc<T>
,RefCell<T>
代表其数据的唯一的所有权。那么是什么让RefCell<T>
不同于像Box<T>
这样的类型呢?回忆一下第四章所学的借用规则:
- 在任意给定时刻,只能拥有一个可变引用或任意数量的不可变引用 之一(而不是两者)。
- 引用必须总是有效的。
对于引用和
Box<T>
,借用规则的不可变性作用于编译时。对于RefCell<T>
,这些不可变性作用于 运行时。对于引用,如果违反这些规则,会得到一个编译错误。而对于RefCell<T>
,如果违反这些规则程序会 panic 并退出。在编译时检查借用规则的优势是这些错误将在开发过程的早期被捕获,同时对运行时没有性能影响,因为所有的分析都提前完成了。为此,在编译时检查借用规则是大部分情况的最佳选择,这也正是其为何是 Rust 的默认行为。
因为一些分析是不可能的,如果 Rust 编译器不能通过所有权规则编译,它可能会拒绝一个正确的程序;从这种角度考虑它是保守的。如果 Rust 接受不正确的程序,那么用户也就不会相信 Rust 所做的保证了。然而,如果 Rust 拒绝正确的程序,虽然会给程序员带来不便,但不会带来灾难。
RefCell<T>
正是用于当你确信代码遵守借用规则,而编译器不能理解和确定的时候。类似于
Rc<T>
,RefCell<T>
只能用于单线程场景。如果尝试在多线程上下文中使用RefCell<T>
,会得到一个编译错误。第十六章会介绍如何在多线程程序中使用RefCell<T>
的功能。如下为选择
Box<T>
,Rc<T>
或RefCell<T>
的理由:
Rc<T>
允许相同数据有多个所有者;Box<T>
和RefCell<T>
有单一所有者。Box<T>
允许在编译时执行不可变或可变借用检查;Rc<T>
仅允许在编译时执行不可变借用检查;RefCell<T>
允许在运行时执行不可变或可变借用检查。- 因为
RefCell<T>
允许在运行时执行可变借用检查,所以我们可以在即便RefCell<T>
自身是不可变的情况下修改其内部的值。在不可变值内部改变值就是 内部可变性 模式。让我们看看何时内部可变性是有用的,并讨论这是如何成为可能的。
🎯内部可变性:不可变值的可变借用
借用规则的一个推论是当有一个不可变值时,不能可变地借用它。例如,如下代码不能编译:
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 | 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 `borrowing` due to previous error
然而,特定情况下,令一个值在其方法内部能够修改自身,而在其他代码中仍视为不可变,是很有用的。值方法外部的代码就不能修改其值了。
RefCell<T>
是一个获得内部可变性的方法。RefCell<T>
并没有完全绕开借用规则,编译器中的借用检查器允许内部可变性并相应地在运行时检查借用规则。如果违反了这些规则,会出现 panic 而不是编译错误。
🎃内部可变性的用例:mock对象
有时在测试中程序员会用某个类型替换另一个类型,以便观察特定的行为并断言它是被正确实现的。这个占位符类型被称为 测试替身(test double)。就像电影制作中的替身演员 (stunt double) 一样,替代演员完成高难度的场景。测试替身在运行测试时替代某个类型。mock 对象 是特定类型的测试替身,它们记录测试过程中发生了什么以便可以断言操作是正确的。
虽然 Rust 中的对象与其他语言中的对象并不是一回事,Rust 也没有像其他语言那样在标准库中内建 mock 对象功能,不过我们确实可以创建一个与 mock 对象有着相同功能的结构体。
如下是一个我们想要测试的场景:我们在编写一个记录某个值与最大值的差距的库,并根据当前值与最大值的差距来发送消息。例如,这个库可以用于记录用户所允许的 API 调用数量限额。
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!"); } } }
这些代码中一个重要部分是拥有一个方法
send
的Messenger
trait,其获取一个self
的不可变引用和文本信息。这个 trait 是 mock 对象所需要实现的接口库,这样 mock 就能像一个真正的对象那样使用了。另一个重要的部分是我们需要测试LimitTracker
的set_value
方法的行为。可以改变传递的value
参数的值,不过set_value
并没有返回任何可供断言的值。我们希望能够说,如果我们创建一个实现了Messenger
trait 和具有特定max
值的LimitTracker
时,当传递不同value
值时,消息发送者应被告知发送合适的消息。我们所需的 mock 对象是,调用
send
并不实际发送 email 或消息,而是只记录信息被通知要发送了。可以新建一个 mock 对象实例,用其创建LimitTracker
,调用LimitTracker
的set_value
方法,然后检查 mock 对象是否有我们期望的消息。示例 15-21 展示了一个如此尝试的 mock 对象实现,不过借用检查器并不允许:#[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
结构体,其sent_messages
字段为一个String
值的Vec
用来记录被告知发送的消息。我们还定义了一个关联函数new
以便于新建从空消息列表开始的MockMessenger
值。接着为MockMessenger
实现Messenger
trait 这样就可以为LimitTracker
提供一个MockMessenger
。在send
方法的定义中,获取传入的消息作为参数并储存在MockMessenger
的sent_messages
列表中。
🎯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>
在任何时候只允许有多个不可变借用或一个可变借用。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)); } }
这里为
borrow_mut
返回的RefMut
智能指针创建了one_borrow
变量。接着用相同的方式在变量two_borrow
创建了另一个可变借用。这会在相同作用域中创建两个可变引用,这是不允许的。当运行库的测试时,示例 15-23 编译时不会有任何错误,不过测试会失败。像我们这里这样选择在运行时捕获借用错误而不是编译时意味着会发现在开发过程的后期才会发现的潜在错误,甚至有可能发布到生产环境才会发现。还会因为在运行时而不是编译时记录借用而导致少量的运行时性能惩罚。然而,使用
RefCell
使得在只允许不可变值的上下文中编写修改自身以记录消息的 mock 对象成为可能。虽然有取舍,但是我们可以选择使用RefCell<T>
来获得比常规引用所能提供的更多的功能。
🎯结合Rc<T>和RefCell<T>来拥有多个可变数据所有者
RefCell<T>
的一个常见用法是与Rc<T>
结合。回忆一下Rc<T>
允许对相同数据有多个所有者,不过只能提供数据的不可变访问。如果有一个储存了RefCell<T>
的Rc<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); }
这里创建了一个
Rc<RefCell<i32>>
实例并储存在变量value
中以便之后直接访问。接着在a
中用包含value
的Cons
成员创建了一个List
。需要克隆value
以便a
和value
都能拥有其内部值5
的所有权,而不是将所有权从value
移动到a
或者让a
借用value
。当我们打印出
a
、b
和c
时,可以看到它们都拥有修改后的值 15 而不是 5:$ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list) Finished dev [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>
的运行时借用规则检查也确实保护我们免于出现数据竞争——有时为了数据结构的灵活性而付出一些性能是值得的。注意RefCell<T>
不能用于多线程代码!Mutex<T>
是一个线程安全版本的RefCell<T>。