内部可变性(Interior mutability)是Rust中的一个设计模式,它允许你在有不可变引用时也可以改变数据。这需要在数据结构中使用unsafe代码来模糊Rust的可变性与借用规则。
可以确保代码在运行时会遵守借用规则,可以使用运用内部可变性模式的类型,涉及unsafe代码被封装进安全的API中。外部类型依旧是不可变的。
使用RefCell在运行时检查借用规则🔗
ReCell<T>代表数据的唯一所有权,RefCell
在一些特定的内存安全场景,虽没违反规则,但是在编译时检查却不被允许,RefCell<T>就派上用场了。
类似于Rc<T>,RefCell<T>也只能用于单线程场景,下面是选择Box<T>、Rc<T>还是RefCell<T>的理由:
Rc<T>允许相同数据有多个所有者,RefCell<T>和Box<T>有单一所有者。Box<T>在编译时执行不可变或可变借用检查,Rc<T>在编译时执行不可变借用检查,RefCell<T>在运行时执行不可变或可变借用检查。- 允许在
RefCell<T>自身不可变的情况下修改其内部的值。
内部可变性用例:mock对象🔗
有时程序员会用某个类型替代另一个类型,观察其某些行为是否被正确实现。这个占位符类型称为测试替身(test double)。mock对象是特定类型的测试替身。
下面是我们想要测试的场景:编写一个记录某个值与最大值差距的库,并根据差距来发送消息,下面是代码:
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: &T, max: usize) -> LimitTracker<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!");
}
}
}
这些代码有一个Massenger trait,它有一个send方法,该方法接受一个字符串切片作为参数。这个trait是mock对象所要实现的接口库。然后我们要测试LimitTracker的set_value方法。
下面是测试代码:
#[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字段,用于存储发送的消息。然后我们实现了Messenger trait,它有一个send方法,该方法接受一个字符串切片作为参数。这个方法将消息添加到sent_messages字段中。这个将limit_tracker的value设置为80,然后我们断言sent_messages字段中有一个消息。
然而测试有问题,因为send方法获取了self的不可变引用,这是我们使用RefCell<T>来解决这个问题:
#[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);
}
}
这里send_messages的字段类型是RefCell<Vec<String>>而不是Vec<String>。
对于send方法的实现,我们使用了borrow_mut方法来获取sent_messages字段的可变引用。这样我们就可以修改sent_messages字段的值了。
最后在assert_eq!宏中,我们使用了borrow方法来获取sent_messages字段的不可变引用。这样我们就可以断言sent_messages字段的值了。
RefCell在运行时记录借用🔗
创建不可变和可变引用时,对RefCell<T>来说是使用borrow和borrow_mut方法。这两个方法都会返回一个Ref<T>和RefMut<T>,它们分别代表不可变和可变引用。
RefCell<T>会记录当前有多少活动的Ref<T>和RefMut<T>,RefCell<T>允许有多个不可变借用或一个可变借用。
结合Rc和RefCell来拥有多个可变数据所有者🔗
Rc<T>允许对相同数据有多个所有者,不过只能提供不可变访问。一个储存了RefCell<T>的Rc<T>可以得到多个所有者并且可以修改的值。下面是链表的例子:
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
use 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列表,用到了克隆使a和value都拥有内部RefCell<i32>的所有权。
同样将a克隆到b和c中,然后我们使用borrow_mut方法来获取value的可变引用并将其值增加10(这里使用了自动解引用得到了RefCell<i32>,然后调用borrow_mut方法来获取i32的可变引用,最后解引用后修改其值)。最后打印出a、b和c的值,可以看到它们的值都被修改了。
使用RefCell<T>我们创建了一个表面上不可变但实际可修改数据的数据结构,同时其规则也保护我们免于数据竞争。
🔥 谨慎
RefCell<T>不能用于多线程代码,将在后面讨论Mutex<T>这个线程安全版本的RefCell<T>。