美文网首页
Rust内部可变性之RefCell

Rust内部可变性之RefCell

作者: 黑天鹅学院 | 来源:发表于2021-03-19 16:59 被阅读0次

    背景

    在Rust中,每个对象(变量)的可见性与可变性均受到所有权的限制,一个对象只能有一个所有者。这个限制对于内存管理来说,无疑是一个非常友善的设计,因为只需要维护好所有者的生命周期,就可以对使用该对象的安全性进行清晰的管理。比如如果对一个对象进行赋值,则伴随者所有权的转让,原有对象失去控制权,可以放心的进行清理。

    可见性与可变性主要体现在对引用的处理上增加了理解与使用的负担,在C语言中,指针的灵活性赋予了开发者施展奇技淫巧的空间,但同时把麻烦推给了开发者。Rust用所有权,生命周期等概念限制了对指针的过度使用,但对于初学者而言,难免感觉到这些限制矫枉过正,严重制约了想象力的施展。

    针对引用,Rust提供了两种类型:

    • &:共享引用
    • &mut:可变引用

    在编译阶段,Rust的行为是,同一作用域内,对于某一个对象的引用,只允许存在两种情况:要么只有一个可变引用,要么同时存在多个共享引用,共享引用不允许修改内容,可变引用才有修改权限。

    比如:

    struct Person {
      name: String,
      age: usize,
    }
    
    fn main() {
      let person = Person { name: "Joe Biden".to_string(), age: 79 };  
      let person_ref: &Person = &person;  
      person_ref.age = 83;
    }
    

    其中,

    person_ref.age = 34;
    

    编译失败,因为person_ref属于共享引用,并没有修改权限。

    再看一个简单的例子:

    fn main() {
        let x = 1;
        let y = &x;
        y = 2;
    }
    

    其中,

    y=2;
    

    报错,因为y属于共享引用。

    编译阶段,Rust borrow checker会对对象修改进行检查,一旦监测到共享引用修改引用指向的内容就会报错,这个检查符合rust的设计原则,但是对于开发者的要求却显得过于苛刻。

    作为面向系统开发的语言,大多数rust开发者很有可能是从C/C++系列迁移而来,在此类语言中,获得一个变量的引用,然后对其进行修改是一件非常正常的事情。

    另一方面,由于 Rust 的 mutable特性, 一个结构体中的字段,要么全都是 immutable,要么全部是mutable,不支持针对部分字段进行设置。比如,在一个struct中,可能只有个别的引用需要修改,而其他变量并不需要修改,为了一个变量而将整个struct变为&mut也是不合理的。

    作为语言规范,尽管Rust在设计范式上开诚布公,也给了开发者明确的预期,但是与大多数人长期形成的习惯进行对抗并非什么明智之举。毕竟,弱小和无知不是生存的障碍,傲慢才是。

    还好Rust的founder们并不是傲慢的人,为了解决这个现实问题,专门引入了内部可变性。

    所谓内部可变性,简单理解,就是赋予共享引用修改的权限,由于这个“赋予”行为是明确指定的,并未违反rust的设计原则。

    内部可变性引入了Cell与RefCell两个wrapper。

    示例

    样例1:

    use std::cell::Cell;
    
    #[derive(Debug)]
    struct Person {
     name: String,
     age: Cell<usize>,
    }
    
    fn main() {
     let person = Person { name: "Joe Biden".to_string(), age: Cell::new(79) };  
     let person_ref: &Person = &person;  
    
     println!("Age is : {:?}", person_ref);
     person_ref.age.set(83);
     println!("Age is : {:?}", person_ref);
    }
    

    样例2:

    use std::cell::Cell;
    fn main() {
        let x = Cell::new(1);
        let y = &x;
        y.set(2);
        println!("{}", x);
    }
    

    也可以使用RefCell来实现:
    样例1:

    use std::cell::RefCell;
    
    #[derive(Debug)]
    struct Person {
      name: String,
      age: RefCell<usize>,
    }
    fn main() {
      let person = Person { name: "Joe Biden".to_string(), age: RefCell::new(79) };  
      let person_ref: &Person = &person;  
      println!("Age is : {:?}", person_ref);
      *person_ref.age.borrow_mut() = 83;
      println!("Age is : {:?}", person_ref);
    }
    

    样例2:

    use std::cell::RefCell;
    fn main() {
        let x = RefCell::new(1);
        let y = &x;
        *y.borrow_mut() =2;
        println!("{}", x);
    }
    

    内部可变性违背了rust关于共享引用的约定,但是通过引入Cell与RefCell,这种走后门的行为是严格备案的,并没有违背原则。

    既然Cell与RefCell都能够实现内部可变性,那这两者之间有什么差异呢?

    Cell与RefCell差异

    首先来看定义:

    struct Cell<T> {
        value: UnsafeCell<T>, 
    }
    
    struct RefCell<T: ?Sized> {
        borrow: Cell<usize>,
        value: UnsafeCell<T>,
    }
    

    RefCell相比Cell,内部维护了一个包装对象的引用计数,当通过RefCell.borrow获取一个共享引用时,内部引用计数加一,当获取的引用离开作用域时,内部引用计数减一,当RefCell.borrow_mut获取一个可变引用时,首先检测引用技数是否为 0,如果为 0,正常返回,如果不为 0,直接 panic,其实RefCell.borrow时也会做类似的检测,当已经获取了可变引用也是直接 panic, 当然为了避免 panic,我们可以用 RefCell.try_borrowRefCell.try_borrow_mut 来获取一个 Result 类型。

    由于Cell并未引入引用计数,所以Cell<T>需要满足T:Copy

    impl<T> Cell<T> where T: Copy {
    
      const fn new(value: T) -> Cell<T>;
      
      // Returns a copy of the contained value.
      fn get(&self) -> T;
      
      // Sets the contained value.
      fn set(&self, val: T);
    }
    

    对于Cell而言,通过get获取到的是原有对象的拷贝,set则使用新的对象替换原有老对象。RefCell<T>没有这个约束,它的操作都是通过返回可变指针完成。

    由于实现机制上的差别,Cell只能包装Copy类型,而RefCell能够包装任意类型,所以在不确定一个对象是否实现了Copy时,应该选择RefCell。

    由于上述差异,RefCell更加常用,通常的做法是配合Rc,组成Rc<RefCell<T>>

    限制

    由于Cell与RefCell均未实现Sync,所以这两种类型均只能用于单线程。

    相关文章

      网友评论

          本文标题:Rust内部可变性之RefCell

          本文链接:https://www.haomeiwen.com/subject/gutbcltx.html