欢迎光临
我们一直在努力

Rust 语言特性:内部可变性(关于 Cell<T> 和 RefCell<T>)

Rust 对不可变引用和可变应用进行了严格的区分,但有时候,我们需要在一个不可变的整体中获得某一个部分的“可变”引用,以方便执行某些操作,特别是当我们非常确定这种局部的可变性不会危害到内存安全时,我们需要 Rust 语言给我们一个“特别许可” 允许我们这样去做,这正是 Cell<T> 和 RefCell<T> 两种容器类型(也可以认为是一种智能指针)提供的功能。真诚推荐本博客近期的两篇精华好文《你真得懂 Rust 借用检查吗?一文聊透 Borrow Checker 底层逻辑:权限模型》和《深入理解 Rust 生命周期(可能是全网最透彻的解读了)》

1. 什么是内部可变性?

内部可变性并不难理解,看个例子就知道了。下面的代码定义了一个 PhoneModel 结构体,它有的一些属性是永远不会变的,例如:date_issued 和 screen_size,但是有些属性却是可能会变化的,例如:on_sale,当一款手机停售以后,这个属性就会从 true 变成 false。尽管在这个例子中,数据发生变化的的周期可能很长,在内存中未必会有一个同等生命周期的结构体一值存在,但是用它来解释数据可能程序运行期间发生变化还是合理的一个示例。

struct PhoneModel {
company_name: String,
model_name: String,
screen_size: f32,
memory: usize,
date_issued: u32,
on_sale: bool,
}

fn main() {
let super_phone_3000 = PhoneModel {
company_name: "YY Electronics".to_string(),
model_name: "Super Phone 3000".to_string(),
screen_size: 7.5,
memory: 4_000_000,
date_issued: 2020,
on_sale: true,
};

}

在上面描述的这种情形下,也就是一个结构体里既有不会发生变化的属性又有可能会变化的属性,通常我们会让结构体整体保持不可变!也就是在定义一个结构体实例时不使用 mut 修饰,就像这样:

let super_phone_3000 = PhoneModel {…}

整体保持不可变性是谨慎和负责的,也是,如何让 on_sale 这个字段单独可变呢?因为这个字段的业务属性决定了它天生就是可变的,而如果我们能保持结构体实例整体不可变的情况下,又能让这个内部属性可变,会给这个结构体带来极大的灵活性,编写代码也会容易很多,这就是对内部可变性的一种典型诉求。

那怎么才能让 on_sale 这个“内部”属性变成可变的呢?答案是:Cell<T>,用它把 on_sale 包裹起来,然后就可以改写它的值了:

use std::cell::Cell;

struct PhoneModel {
…
on_sale: Cell<bool>,
}

fn main() {
let super_phone_3000 = PhoneModel {
…
on_sale: Cell::new(true),
};

// 虽然 super_phone_3000 是不可变的,但它的“内部”也就是 on_sale 属性却是可变的!
super_phone_3000.on_sale.set(false);
}

正如同代码注释所说:虽然 super_phone_3000 是不可变的,但它的“内部”也就是 on_sale 属性却是可变的!这就是“内部可变性”。

2. 关于 Cell<T> 的更多信息

在上一节的简单示例中,我们只是初步认识了 Cell<T>,本节继续深入了解一下。Cell<T> 的官方文档是:https://rustwiki.org/zh-CN/core/cell/struct.Cell.html。 Cell<T> 的一大特点是:绝不暴露它包裹的值!即使你看到我们可以通过它的 get() 和 set() 方法去读写它内部的值时,也不是把它内部的值“暴露”出来,而是直接发生了内部包裹值的整体替换、移动或复制。Cell<T> 对内部包裹值的处理可以分成三种情况:

  • 将内部值复制出来

    • 例如:Cell<T: Copy>::get()
  • 将内部值替换出来

    • 例如:Cell<T: Default>::take() 🠚 取出内部值,并以该类型的默认值进行填充(例如 i32 默认值是 0)
    • 例如:Cell<T>::replace() 🠚 取出内部值,并用指定的值填充回去
  • 将内部值取出,同时将所有权也 move 出来,即:这会导致 Cell 失效

    • 例如:Cell<T>::into_inner() 🠚 取出内部值,且内部值的所有权也一并 move 了出来,Cell 将失效

关于 Cell<T>,网上有一种非常肤浅的认识,认为:Cell<T> 是专门针对 Copy 类型的,它执行的是 copy 语义!这个认识是不准确的,因为上面的示例已经说明它除了 copy 之外,还可以 replace 或 move。Cell<T> 设计的关键立足点是:不对外暴露值!

3. 另一种内部可变性容器 RefCell<T>

与 Cell<T> 相近的另一种内部可变性容器是 RefCell<T>,它同样可以在不声明 mut 的情况下去改写它包裹的值,但它是通过引用去改写值的,而且它还能对外借出包裹值的可变引用,而不像 Cell<T> 那样是直接复制或替换值本身。还是通过一个示例来解释:

use std::cell::RefCell;

#[derive(Debug)]
struct User {
id: u32,
year_registered: u32,
username: String,
active: RefCell<bool>
}

fn main() {
let user_1 = User {
id: 1,
year_registered: 2020,
username: "User 1".to_string(),
active: RefCell::new(true)
};

println!("{:?}", user_1.active);
}

上面的示例与前一个 PhoneModel 示例几乎是一样的,只是将可变字段从使用 Cell<T> 包裹改为了用 RefCell<T> 包裹。RefCell<T> 既提供了直接改写包裹值的方法也提供了借出包裹值的可变引用的方法。以下是一些示例:

  • 使用 replace() 方法直接改写值

user_1.active.replace(false);

  • 使用 replace_with() 方法执行闭包逻辑改写值

user_1
.active
.replace_with(|_| if date < 2000 { true } else { false });

  • 使用 borrow() 借出不可变引用

let r = user_1.active.borrow();

  • 使用 borrow_mut() 借出可变引用

let r = user_1.active.borrow_mut();

要特别提醒注意的是:RefCell<T> 在借出引用时依然会严格遵守借用规则,即:只能借出一个可变引用或多个不可变引用,不允许同时借出可变引用和不可变引用。此外,RefCell<T> 的另一个显著特点是:它是在运行时进行借用检查的,而不是在编译时,这一点非常独特!

4. Cell<T> 和 RefCell<T> 的异同

介绍完两种内部可变性容器后,我们来梳理一下它们之间的异同:

异同点Cell<T>RefCell<T>
对值的操作方式 对内部值的修改不是通过获得内部值的引用进行的,而是通过复制、移动、替换整个值来完成的。 对内部值的修改是通过引用完成的,且可以对外借出值的可变引用(也包括不可变引用)
执行借用检查的时机 编译期 运行期

5. 为什么需要内部可变性?

前面我们泛泛地提到过内部可变性的必要性,这一节,我们专门来研究一下这个问题。看这样一个例子:我们要设计一个 Messenger 系统,它是一个 trait,代表一种发送消息的行为能力,所以它有一个 send 方法,整体定义是这样:

pub trait Messenger {
fn send(&self, msg: &str);
}

系统可以有实现多种不同的 messenger,例如:EmailMessenger 或 TextMessenger 等。同时,系统还有一个检测某个数值(比如 API 调用次数)与设定的最大值差距的工具类 LimitTracker,当数值分别达到最大预设值的 75%、90%、100% 时,它要通过 Messenger 对外发送通知。LimitTracker 的具体实现如下:

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!");
}
}
}

我们说过,系统可能会 EmailMessenger 或 TextMessenger 等具体实现,但是,在为了能进行单元测试,我们需要编写一个 MockMessenger 来测试 LimitTracker 的功能,我们并不需要这个 MockMessenger 真得发送消息,它需要能把要发送的信息保存起就够了,这个 MockMessenger 的具体实现是这样的:

#[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);
}
}

它包含一个 sent_messages: Vec<String> 字段,用来存储需要由它发送的消息,同时,它也必须实现 Messenger 的 send 方法,就是在接收到消息候,把它添加到 vec 里就可以了。但是上面的代码会在第 17 行报错:

Cannot borrow immutable local variable `self.sent_messages` as mutable

因为 Messenger 中定义的 send 方法接受的是一个 &self 参数,是对 Messenger 实例的一个不可变引用,但是,在 MockMessenger 的实现中,由于需要将消息添加到 sent_messages 中,所以动用了它的 push 方法,而这需要的是一个 sent_messages 的可变引用,由于整体,也就是 MockMessenger 实例已经是不可变的,这就导致了冲突。

其实这里,对于 sent_messages 是一个典型的“内部可变性”诉求,也就是在保持 MockMessenger 实例整体不可变的前提下,唯独需要 sent_messages是一个可变类型,且作为一个测试用例,这种可变性没有任何地危害,只是编译器那里通过不了,这就是 RefCell<T> 的典型应用场景了,我们只需要使用 RefCell<T> 将 sent_messages 包裹一下,然后在需要改变值的地方使用 borrow_mut() 获得可变引用去修改值,一切就都解决了:

mod tests {
use super::*;
use std::cell::RefCell;

struct MockMessenger {
// 使用 `RefCell<T>` 将 `sent_messages` 包裹一下,赋予它内部可变性
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) {
// 通过 borrow_mut() 获得可变引用去修改值
self.sent_messages.borrow_mut().push(String::from(message));
}
}

#[test]
fn it_sends_an_over_75_percent_warning_message() {
// –snip–
// 简单的读取操作使用 borrow() 获得不可变引用即可
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}

参考资料:

https://highassurance.rs/chp3/rust_5_own_2.html https://xuanwu.csdn.net/6964b0d20846ec2c4c5b05a9.html https://rustcc.cn/article?id=a9204a0d-fbc7-4b68-9a30-7f4147e35f78 https://colobu.com/easy-rust/Chapter_41.html

赞(0)
未经允许不得转载:171主机测评 » Rust 语言特性:内部可变性(关于 Cell<T> 和 RefCell<T>)
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址