Rust从入门到实战系列一百七十八:内部可变性:不可变值的可变借用

借用规则的一个推论是当有一个不可变值时,不能可变地借用它。例如,如下代码不能编译:
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 是一个获得内部可变性的方法。RefCell 并没有完
全绕开借用规则,编译器中的借用检查器允许内部可变性并相应地在运行时检查借用规则。如果违反了
这些规则,会出现 panic 而不是编译错误。
让我们通过一个实际的例子来探索何处可以使用 RefCell 来修改不可变值并看看为何这么做是有意
义的。
内部可变性的用例:mock 对象
测试替身(test double)是一个通用编程概念,它代表一个在测试中替代某个类型的类型。mock 对象
是特定类型的测试替身,它们记录测试过程中发生了什么以便可以断言操作是正确的。
虽然 Rust 中的对象与其他语言中的对象并不是一回事,Rust 也没有像其他语言那样在标准库中内建
mock 对象功能,不过我们确实可以创建一个与 mock 对象有着相同功能的结构体。
如下是一个我们想要测试的场景:我们在编写一个记录某个值与最大值的差距的库,并根据当前值与最
大值的差距来发送消息。例如,这个库可以用于记录用户所允许的 API 调用数量限额。
该库只提供记录与最大值的差距,以及何种情况发送什么消息的功能。使用此库的程序则期望提供实际
发送消息的机制:程序可以选择记录一条消息、发送 email、发送短信等等。库本身无需知道这些细节;
只需实现其提供的 Messenger trait 即可。示例 15-20 展示了库代码:
文件名: 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: &T, max: usize) -> LimitTracker {
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!”);
}
}
}
示例 15-20:一个记录某个值与最大值差距的库,并根据此值的特定级别发出警告
这些代码中一个重要部分是拥有一个方法 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 对象实现,不过借用检
查器并不允许:
文件名: src∕lib.rs

pub trait Messenger {

fn send(&self, msg: &str);

}

pub struct LimitTracker<'a, T: Messenger> {

messenger: &'a T,

value: usize,

max: usize,

}

REFCELL 和内部可变性模式 399

impl<'a, T> LimitTracker<'a, T>

where

T: Messenger,

{

pub fn new(messenger: &T, max: usize) -> LimitTracker {

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,
}
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 结构体,其 sent_messages 字段为一个 String 值的 Vec 用来记录
被告知发送的消息。我们还定义了一个关联函数 new 以便于新建从空消息列表开始的 MockMessenger
值。接着为 MockMessenger 实现 Messenger trait 这样就可以为 LimitTracker 提供一个 MockMessenger。
在 send 方法的定义中,获取传入的消息作为参数并储存在 MockMessenger 的 sent_messages 列表中。
在测试中,我们测试了当 LimitTracker 被告知将 value 设置为超过 max 值 75% 的某个值。首先新建一
个 MockMessenger,其从空消息列表开始。接着新建一个 LimitTracker 并传递新建 MockMessenger
的引用和 max 值 100。我们使用值 80 调用 LimitTracker 的 set_value 方法,这超过了 100 的 75%。接
着断言 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
|
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…
error: build failed
不能修改 MockMessenger 来记录消息,因为 send 方法获取了 self 的不可变引用。我们也不能参考错
误文本的建议使用 &mut self 替代,因为这样 send 的签名就不符合 Messenger trait 定义中的签名了
(可以试着这么改,看看会出现什么错误信息)。
这正是内部可变性的用武之地!我们将通过 RefCell 来储存 sent_messages,然后 send 将能够修改
sent_messages 并储存消息。示例 15-22 展示了代码:
文件名: 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: &T, max: usize) -> LimitTracker {

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>,
}
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);
}
}
示例 15-22:使用 RefCell 能够在外部值被认为是不可变的情况下修改内部值
现在 sent_messages 字段的类型是 RefCell<Vec> 而不是 Vec。在 new 函数中新建了
一个 RefCell<Vec> 实例替代空 vector。
对于 send 方法的实现,第一个参数仍为 self 的不可变借用,这是符合方法定义的。我们调用
self .sent_messages 中 RefCell 的 borrow_mut 方法来获取 RefCell 中值的可变引用,这是一个 vector。
接着可以对 vector 的可变引用调用 push 以便记录测试过程中看到的消息。
最后必须做出的修改位于断言中:为了看到其内部 vector 中有多少个项,需要调用 RefCell 的 borrow
以获取 vector 的不可变引用。
现在我们见识了如何使用 RefCell,让我们研究一下它怎样工作的!

  • 9
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值