理解Cell和RefCell
Cell 和 RefCell 的作用
Rust 的借用规则(同一时刻,只能拥有一个可变引用,或者多个不可变引用)带来安全提升的同时,却也损失了灵活性,在有些时候,我们可能需要对一个不可变引用的目标进行数据的修改,这对于正常的 Rust 代码来说,是不可能实现的。所以 Rust 提供了 Cell 和 RefCell 用于内部可变性。
内部可变性
简单来说,对一个不可变的值进行可变借用,但这个并不符合 Rust 的基本借用规则
fn main() {
let x = 5;
let y = &mut x; // 报错
}
// --------------------------
fn main(){
let mut x = 1;
let y = &x; // 可以对一个可变值进行不可变借用。
let z = &x;
println!("{x},{y},{z}"); // 1,1,1
x += 1; // 修改前所有不可变借用已 drop
println!("{x}"); // 2
}我们不能对一个不可变的值进行可变借用,这会破坏 Rust 的安全性保证,相反,可以对一个可变值进行不可变借用。原因是:当值不可变时,可能存在多个不可变的引用指向它,此时若将其中一个修改为可变的,会造成可变引用与不可变引用共存的情况;而当值可变时,最多只会有一个可变引用指向它。
一个 Messenger 的例子
// 定义在外部库中的特征
pub trait Messenger {
fn send(&self, msg: String);
}
// --------------------------
// 我们的代码中的数据结构和实现
struct MsgQueue {
msg_cache: Vec<String>,
}
impl Messenger for MsgQueue {
fn send(&self, msg: String) {
self.msg_cache.push(msg)
}
}如上所示,外部库中定义了一个消息发送器特征 Messenger,它只有一个发送消息的功能:fn send(&self, msg: String),因为发送消息不需要修改自身,因此原作者在定义时,使用了 &self 的不可变借用,这个无可厚非。
而我们要在自己的代码中使用该特征实现一个异步消息队列,出于性能的考虑,消息先写到本地缓存(内存)中,然后批量发送出去,因此在 send 方法中,需要将消息先行插入到本地缓存 msg_cache 中。但是由于该 send 方法的签名是 &self(该定义在外部库中,不能修改为 &mut self),因此上述代码会报错。
Cell
Cell 和 RefCell 在功能上没有区别,区别在于 Cell<T> 适用于 T 实现 Copy 的情况:
use std::cell::Cell;
fn main() {
let c = Cell::new(1);
let one = c.get(); // 因为实现了 Copy,get 时会 copy 一份当前状态
c.set(2); // 这里的 set 并不影响 one 的数据
let two = c.get();
println!("{},{}", one, two); // 1,2
let three = &c; // 和 get 不同,这里通过借用,本质指向同一个地址 c
three.set(3); // 同 c.set()
println!("{},{},{},{}", one, two, c.get(), three.get()); // 1,2,3,3,3
}
// --------------------------
fn main(){
let c = Cell::new(String::from("1")); // 报错,因为 String 没有实现 Copy 特征
}RefCell
由于 Cell 类型针对的是实现了 Copy 特征的值类型,因此在实际开发中,Cell 使用的并不多,因为我们要解决的往往是可变、不可变引用共存导致的问题,此时就需要借助于 RefCell 来达成目的。
用 RefCell 修改上面所说的 Messenger 例子:
use std::cell::RefCell;
pub trait Messenger {
fn send(&self, msg: String);
}
pub struct MsgQueue {
msg_cache: RefCell<Vec<String>>, // 用 RefCell 包裹
}
impl Messenger for MsgQueue {
fn send(&self, msg: String) {
self.msg_cache.borrow_mut().push(msg) // 利用 RefCell 让 &self 中的 msg_cache 成为一个可变值,然后实现对其的修改
}
}
fn main() {
let mq = MsgQueue {
msg_cache: RefCell::new(Vec::new()),
};
mq.send("hello, world".to_string());
}需要注意的是:使用 RefCell 可以绕过编译器借用规则,实现编译期可变、不可变引用共存:
let b = RefCell::new(String::from("Hello world"));
let s1 = s.borrow();
let s2 = s.borrow_mut(); // 编译器不报错
println!("{},{}", s1, s2);❗️注意
RefCell 实际上并没有解决可变引用和引用可以共存的问题,只是将报错从编译期推迟到运行时,从编译器错误变成了 panic 异常,上面代码运行时依然会抛出异常,你需要确保自己的代码是正确的
总结
Cell只适用于Copy类型(常见的 Copy 类型见 Rust 中的拷贝和克隆),用于提供值,而RefCell用于提供引用Cell不会panic,而RefCell会,RefCell只是将借用规则从编译期推迟到程序运行期,并不能帮你绕过这个规则RefCell适用于需要内部可变性时或者一个引用被在多处代码使用、修改以至于难于管理借用关系时,用来简化程序代码- 优先使用
Cell,性能更佳