聊聊Rust的Cell和RefCell

2023-11-27 12:30:43 浏览数 (2)

文章目录

  • Why interior mutability?
    • 修改结构体的字段
  • Cell 只适合 Copy 类型
  • RefCell 提供引用
  • 运行时检查

内部可变性(interior mutability)是Rust用来表示在一个值的外部看起来是不可变的,但是在内部是可变的。这种模式通常用于在拥有不可变引用的同时修改目标数据。

CellRefCellRust提供的两种内部可变性的实现。Cell是用于Copy类型的,而RefCell是用于非Copy类型的。

不知道你有没有好奇过具体内部可变性应用在什么场景,为啥要分两种实现。

今天我们针对一些场景来聊聊这两个类型的应用。

Why interior mutability?

如下代码所示,当需要多个可变引用时,会违反Rust的所有权要求:同一时间只能有一个可变引用。

代码语言:javascript复制
let mut x = 1;
let y = &mut x;
let z = &mut x;
x = 2;
*y = 3;
*z = 4;
println!("{}", x);
# will get error:
# error[E0499]: cannot borrow `x` as mutable more than once at a time
#  --> src/main.rs:5:9
#   |
# 4 | let y = &mut x;
#   |         ------ first mutable borrow occurs here
# 5 | let z = &mut x;
#   |         ^^^^^^ second mutable borrow occurs here
# 6 | x = 2;
# 7 | *y = 3;
#   | ------ first borrow later used here

# error[E0506]: cannot assign to `x` because it is borrowed
#  --> src/main.rs:6:1
#   |
# 4 | let y = &mut x;
#   |         ------ `x` is borrowed here
# 5 | let z = &mut x;
# 6 | x = 2;
#   | ^^^^^ `x` is assigned to here but it was already borrowed
# 7 | *y = 3;
#   | ------ borrow later used here

这个时候就是内部可变性发挥作用的时候了。拿Cell来举例

代码语言:javascript复制
let x = Cell::new(1);
let y = &x;
let z = &x;
x.set(2);
y.set(3);
z.set(4);
println!("{}", x.get());

通过Cell,其封装了getset,可以在不需要显示声明为可变的情况下修改值。

修改结构体的字段

一般我们要修改一个结构体的值,需要将其声明为mut, 而对应的方法也需要接收&mut self 举例如下:

代码语言:javascript复制
#[derive(Debug, Default)]
struct Person {
    age: u32,
    name: String,
}

impl Person {
    fn celebrate_birthday(&mut self) {
        let current_age = self.age;
        self.age = current_age   1;
    }
}
let mut Person = Person::default();
Person.celebrate_birthday();
println!("Age after birthday: {}", Person.age);

但是有时候我们并不想这么做,因为我们只是想修改其中的某个字段,而不是整个结构体,亦或者接口并不想暴露一个&mut self的方法

代码语言:javascript复制
#[derive(Debug, Default)]
struct Person {
    age: Cell<u32>,
    name: String,
}

impl Person {
    // 方法receiver无需声明为`mut`
    fn celebrate_birthday(&self) {
        let current_age = self.age.get();
        self.age.set(current_age   1);
    }
}
person.celebrate_birthday();
println!("Age after birthday: {}", person.age.get());

Cell 只适合 Copy 类型

对于非Copy类型,Cell并不适用, 因为其约束了get方法的返回值必须是Copy类型。

代码语言:javascript复制
impl<T: Copy> Cell<T> {
    pub fn get(&self) -> T {

那是不是不能往Cell里面放非Copy类型的值呢?当然不是,只是失去了意义,代码如下

代码语言:javascript复制
let mut s = Cell::new(String::from("value"));
// 没有 `s.get()`,因为 `String` 不是 `Copy` 类型
// 而`get_mut()`返回的是 要求自身是可变的,就失去了用`Cell`的意义
*s.get_mut() = String::from("value2");
println!("{}", s.into_inner());

RefCell 提供引用

RefCell主要的不同是支持非Copy类型,且返回的是引用,而不是值。

代码语言:javascript复制
use std::cell::RefCell;

let c = RefCell::new("hello".to_owned());
*c.borrow_mut() = "bonjour".to_owned();
let val = c.borrow();

assert_eq!(&*val, "bonjour");

运行时检查

如果把上边代码换成如下先借用,编译能通过,但是运行时会报错。

RefCell 依旧要遵守借用规则,只是推迟检查从编译期到运行时,如果违反了借用规则,会 panic

代码语言:javascript复制

```rust
use std::cell::RefCell;

let c = RefCell::new("hello".to_owned());
let val = c.borrow(); // 先借用再修改,最后读取借用的值
*c.borrow_mut() = "bonjour".to_owned();

assert_eq!(&*val, "bonjour");

# will panic:
# thread 'main' panicked at 'already borrowed: BorrowMutError', src/main.rs:7:8

综上可以看出,CellRefCell是不同粒度的内部可变性实现,简单的Copy类型可以考虑开销小的Cell来获取有内部可变性的, 需要更灵活的内部可变借用就要用RefCell


推荐阅读

  • 掌握Rust:从零开始的所有权之旅
  • 深度解读-如何用keycloak管理external auth

如果有用,点个 在看 ,让更多人看到

外链不能跳转,戳 阅读原文 查看参考资料

0 人点赞