Cell<T> 与 RefCell<T> 的内部可变性

在编译阶段, Rust会限制同一作用域内同一对象的引用要么是一个可变引用要么是多个不可变引用

Try in Playground

Rust展开
123456789101112
#[derive(Debug)]
struct People {
    name: String,
    age: u8,
}

fn main(){
    let p = People{name: "roadup".to_string(), age: 18};
    let a = &p;
    a.age = 19;
    println!("People: {:?}", a);
}

编译上述代码会报错

Rust展开
1234567
error[E0594]: cannot assign to `a.age`, which is behind a `&` reference
  --> src/main.rs:10:5
   |
9  |     let a = &p;
   |             -- help: consider changing this to be a mutable reference: `&mut p`
10 |     a.age = 19;
   |     ^^^^^^^^^^ `a` is a `&` reference, so the data it refers to cannot be written

编译阶段, Rust会对修改行为进行检查, 不可变引用修改指向的内容会报错.

对于结构体来说, 其所包含的字段要么都是可变的, 要么都是不可变的, 不支持对部分字段设置可变性

为了能支持部分字段的可修改性, 因此引入了CellRefCell, 这便是内部可变性

Try in playground

Rust展开
123456789101112131415161718192021
use std::cell::{Cell, RefCell};

#[derive(Debug)]
struct People {
    name: String,
    age: Cell<u8>,
    height: RefCell<u8>
}

fn main(){
    let p = People{
        name: "roadup".to_string(), 
        age: Cell::new(18), 
        height: RefCell::new(170)
    };
    
    let a = &p;
    a.age.set(19);
    *a.height.borrow_mut() = 180;
    println!("People: {:#?}", a);
}
Rust展开
123456789
People: People {
    name: "roadup",
    age: Cell {
        value: 19,
    },
    height: RefCell {
        value: 180,
    },
}

可以看到虽然结构体是不可变的, 引用也是不可变的, 但可以通过Cell来改变部分属性的可修改性

它本质上是违反了Rust的不可变引用的规则, 但是通过 Cell/RefCell 标记了这一行为, 相当于对这一走后门的行为做严格备案. 这并不意味着Rust放弃了借用规则, 只是将借用检查放到了运行时处理.

Cell 定义

Rust展开
123
pub struct Cell<T: ?Sized> {
    value: UnsafeCell<T>,
}

RefCell与Cell的区别在于, RefCell内部维护了引用状态

Rust展开
123456789101112
type BorrowFlag = isize;

pub struct RefCell<T: ?Sized> {
    borrow: Cell<BorrowFlag>,
    // Stores the location of the earliest currently active borrow.
    // This gets updated whenever we go from having zero borrows
    // to having a single borrow. When a borrow occurs, this gets included
    // in the generated `BorrowError/`BorrowMutError`
    #[cfg(feature = "debug_refcell")]
    borrowed_at: Cell<Option<&'static crate::panic::Location<'static>>>,
    value: UnsafeCell<T>,
}

但使用 RefCell::borrow_mut 获取一个可变引用的时候, 内部会先对引用状态检查, 如果为0就正常返回, 如果不为0则会 panic ,RefCell::borrow 也类似,已经获取了可变引用的时候会panic.

可以通过RefCell::try_borrowRefCell::try_borrow_mut获取Result类型, 以避免panic.

这些检查是在运行时实现的, 因此会存在一定的性能损失.

由于Cell未使用引用计数, 因此对于类型 T 必须实现 Copy trait

Rust展开
12345678
impl <T> Cell<T> where T : Copy {

    const fn new(value: T): Cell<T>;

    fn get(&self) -> T // return a copy
    
    fn set(&self, val: T) // replace with val
}

对与Cell而言, get方法获取的是内部值的一个拷贝, set方法将会用传入的值替换内部的值.

RefCell 操作的是原始对象的指针, 因此不存在拷贝的问题, 也不需要T实现 Copy trait.

由于实现的不同, RefCell会更常用, 通常配合Rc使用 Rc<RefCell<T>>.

另外, Cell和RefCell 均为未实现 Sync , 因此他们都只能在单线程使用

- roadup -