Rust 学习笔记(29)-RefCell

参考章节《Rust 程序设计语言》第15.5章 RefCell<T> 和内部可变性模式

《Rust 程序设计语言》上这一章,我实在是懒得吐槽,举的例子太啰嗦(复杂),这一章不建议看这本书

关于这一章,我推荐看下面这本开源

参考章节《Rust语言圣经(Rust Course)》第3.4.5章 Cell 和 RefCell 内部可变性

内部可变性

好了我们先来引出问题

假如有一个外部库,它定义了一个消息发送器 Messenger,它有一个 send 方法,用于发送一条消息

1
2
3
pub trait Messenger {
    fn send(&self, msg: String);
}

我们要在自己的代码中实现这个 trait

出于性能的考虑,消息先写到本地缓存(内存)中,然后批量发送出去,因此在 send 方法中,需要将消息先行插入到本地缓存 msg_cache 中。

但是问题来了,该 send 方法的签名是 &self

因为发送消息不需要修改自身,因此原作者在定义时,使用了 &self 的不可变借用,这个无可厚非

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
pub struct MsgQueue {
    msg_cache: Vec<String>,
}

// 实现 Messenger
impl Messenger for MsgQueue {
    fn send(&self, msg: String) {
        self.msg_cache.push(msg) // 这里无法修改 msg_cache 因为 &self 是不可变借用
    }
}

我们上面的代码不能通过编译,原因很简单,因为 &self 是不可变的,我们总不能让库的作者去修改他的代码吧

此时就可以利用 RefCell<T>

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
use std::cell::RefCell;

pub struct MsgQueue {
    msg_cache: RefCell<Vec<String>>, // RefCell 包裹一下我们的 Vector
}

impl Messenger for MsgQueue {
    fn send(&self, msg: String) {
        self.msg_cache.borrow_mut().push(msg) // borrow_mut()方法获取一个可变借用,然后我们就可以修改 msg_cache
    }
}

fn main() {
    let mq = MsgQueue {
        msg_cache: RefCell::new(Vec::new()),
    };
    mq.send("hello, world".to_string());
}

由于本身我们的send()方法是&self不可变引用,因此正常情况下,我们不能改变其内部成员的值,所以我们使用了RefCell<T>
RefCell<T>允许我们对一个不可变的值进行可变借用,所以我们在send()方法内部,获取了msg_cache可变借用,从而使msg_cache可以修改

但它对于外部代码仍然是不可变的

1
2
1. 第一是因为 mq 是不可变的借用,因此你不能去修改其内部的成员
2. 第二是因为 msg_cache 没有用 pub 关键字修饰暴露出去(外部都拿不到还改个锤子)

这种内部对一个不可变的值进行可变借用,叫做内部可变性,这是一种设计模式,它和RefCell<T>没有必然联系,但RefCell<T>实现了内部可变性

什么叫做RefCell<T>实现了内部可变性? 请看如下代码

1
2
3
4
5
6
7
8
use std::cell::RefCell;

fn main() {
    let counter = RefCell::new(0);
    let mut number = counter.borrow_mut();
    *number += 1;
    println!("num = {}", number); // 1
}

虽然 counter 是不可变的,不过我们仍然可以获取其内部值的可变引用,所以说RefCell<T>实现了内部可变性

RefCell 到底做了什么事

接下来我们来看看 RefCell 到底做了什么事情

首先,RefCell<T> 内部使用了不安全的代码来模糊Rust的借用检查规则,这一点知道就行了,我们还没有学习到 unsafe
其次,RefCell<T>检查借用规则步骤从编译时期转移到了运行时期

我们回忆一下借用规则

  1. 在任何给定时间里,要么只能拥有一个可变引用,要么只能拥有任意数量的不可变引用
  2. 引用必须总是有效

在编译期检查借用规则,当不满足时出现错误,而在运行时检查借用规则,如过不满足,则panic

下图展示了BoxRcRefCell之间的区别,它出自杨旭的Rust视频

Box、Rc、RefCell的区别

好了知道了这些后,我们来看看,上面的代码,为什么我们使用 RefCell<T> 后,程序就能编译通过了

首先在上面的定义中 msg_cache 本身是不可变的(因为实例引用是&self)这一点没问题吧
然后我们通过 borrow_mut() 获取了一个 msg_cache 可变借用,但根据借用规则是不允许可变引用不可变引用共存的
这种情况下正常是不允许编译的(不允许你获取它的可变引用),但RefCell<T>检查步骤从编译时转移到了运行时,此时程序就能正常编译通过了

Rc和RefCell组合使用

下面的例子是原封不动照搬《Rust语言圣经》,因为我觉得这个例子举的很好理解,比《Rust 程序设计语言》中的例子好的多

在 Rust 中,一个常见的组合就是 RcRefCell 在一起使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
    let s = Rc::new(RefCell::new("我很善变,还拥有多个主人".to_string()));

    let s1 = s.clone();
    let s2 = s.clone();
    s2.borrow_mut().push_str(", on yeah!");

    println!("{:?}\n{:?}\n{:?}", s, s1, s2);
}

上面代码中,我们使用 RefCell<String> 包裹一个字符串,同时通过 Rc 创建了它的三个所有者:ss1s2,并且通过其中一个所有者 s2 对字符串内容进行了修改。
由于 Rc 的所有者们共享同一个底层的数据,因此当一个所有者修改了数据时,会导致全部所有者持有的数据都发生了变化。

运行结果

1
2
3
RefCell { value: "我很善变,还拥有多个主人, on yeah!" }
RefCell { value: "我很善变,还拥有多个主人, on yeah!" }
RefCell { value: "我很善变,还拥有多个主人, on yeah!" }

总结一下

  1. 内部可变性是一种设计模式
  2. RefCell<T> 允许我们对一个不可变的值进行可变借用
  3. RefCell<T>借用规则检查从编译期转移到了运行时
  4. RefCell<T> 只是转移了借用规则检查的时机,但并不是不检查了,如果不满足要求则会触发panic

例如下面这样是不允许的,这将触发 panic

1
2
3
4
5
6
7
8
9
impl Messenger for MsgQueue {
    fn send(&self, msg: String) {
        let mut one_borrow = self.msg_cache.borrow_mut(); // 根据借用规则,不允许同时拥有两个可变引用
        let mut two_borrow = self.msg_cache.borrow_mut(); // 根据借用规则,不允许同时拥有两个可变引用

        one_borrow.push(msg.to_string());
        two_borrow.push(msg.to_string());
    }
}
updatedupdated2025-03-012025-03-01