Rust标准库之叛逆的容器:Cell<T> 和 RefCell<T>

原创
2017/11/14 10:55
阅读数 2.3W

基础概念

在介绍这两个容器之前,请随我一起复习一下Rust中的一些概念和规则(已经熟悉的可以直接跳过)。

Rust语言的资源管理采用所有权的方式来管理,一个对象(值)只有一个所有者,跟踪所有者的生存期,可以对对象(值)的使用安全性进行控制,赋值操作具有转移语义,如果将一个对象赋值给一个变量,则新变量获得该对象的所有权,这是一个比较理想的模型,现实编程中,有很多场景需要引用到一个对象,于是提出了引用类型,引用共有两种共享引用可变引用。这些概念听起来比较复杂,我们先来听个故事就好理解了。

有一天你买了一本书,你目前是该书的所有者,你拥有该书的所有权,如你把书送(转移)给表妹了,那你表妹就成了该书的所有者,她现在拥有该书的所有权,而除了得到表妹的好感之外,和这本书已经没有半毛钱关系了。你表妹是一个人长得很漂亮的巫师,精通各种巫术,很多男生为了与她搭讪,经常来找她借这本书看,可是书一次只能借给一个人看,有些男生因为借不到书而不开心,于是你表妹发明了一种魔镜(引用),只要拿到魔镜的人都可以同时看这本书,有些男生为了向你表妹示爱,会时不时的通过魔镜在你表妹的书上写一段情话,每天看着这些情话你表妹可开心了!可是慢慢的又出现问题了,有男人看到别人写的这些肉麻的情话,很是吃醋,于是别人在写的时候他就干坏事儿,也在同一个地方写,让你表妹的书里充满了诸如“我爱(吃屎吧)你”这样神经错乱的话语。这让你表妹感到很恼火,为了解决这种混乱,她重铸了所有魔镜为普通魔镜(共享引用),并发明了一把特权魔镜(可变引用),拿到普通魔镜(共享引用)的人只能看书的内容,不可以写,而拿到特权魔镜(可变引用)的人可以看又可以写,但是特权魔镜只有一把,一次只给一个人,而且你表妹还规定,特权魔镜和普通魔镜不可以同时借出,也就是说如果已经有人在使用普通魔镜看书,那么特权魔镜不借,如果特权魔镜已经借出,那么普通魔镜不借。你还别说,这招还真管用,从此你表妹过上了幸福的生活,当然和你没有任何关系。

现在应该比较清楚了吧!让我们忘掉表妹,回到现实!

引用分为可变引用(&mut T)和共享引用(&T),可变引用与本文无关,我们先不讨论。来看看共享引用的特点,如上所述,一个对象上可以创建和同时存在多个共享引用,但看都只能读取对象内容,不可以修改对象内容。

struct Person {
  name: String,
  age: usize,
}

fn main() {
  let person = Person { name: "zengsai".to_string(), age: 18 };
  //    ^         ^
  //    |         |
  // 所有者     对象或值
  
  let person_ref_1: &Person = &person;
  //    ^ 
  //    |
  // 共享引用1
  
  let person_ref_2: &Person = &person;
  //    ^ 
  //    |
  // 共享引用2
  
  println!("Name is : {}", person_ref_1.name);  // OUTPUT: Name is : zengsai
  println!("Age is : {}", person_ref_2.age);    // OUTPUT: Age is : 18
  //                                    ^
  //                                    |
  //                         都可以访问对象的内容
  
  person_ref_1.age = 34;   // ERROR: 通过共享引用修改对象内容
}

内部可变性

以上是Rust对所有共享引用的强制限制,但实际使用中,还是有一些需要通过共享引用来修改对象内容的场景,比如实现一些逻辑上不修改对象内容但实质上要修改对象内容的方法,为了表达逻辑上不修改该对象,我们把对象的共享引用传递给方法,在方法中就只能读取对象内容而不能修改对象,这种情况可以看Clone Trait(先想象成Java中的Interface)。

pub trait Clone {
  fn clone(&self) -> Self;
}

impl Clone for Person {
    fn clone(&self) -> Self {
      Person {
        name: self.name.clone(),
        age: self.age
      }
    }
}

在Person的克隆方法体中,所有点都只读取了原对象内容,没有做修改,也不允许做修改。但如果我要能在Person中记录目前一共有多少份克隆数据怎么办呢?这意味着每次克隆时我们需要增加计数器。

struct Person {
  name: String,
  age: usize,
  count: usize, // 一共有多少个副本,包含自身
}

impl Clone for Person {
    fn clone(&self) -> Self {
      let new_count = self.count + 1;
      
      Person {
        name: self.name.clone(),
        age: self.age,
        count: new_count,
      }
      
      self.count = new_count;   // ERROR: 通过共享引用修改对象内容
    }
}

非常不幸,以上代码无法编译通过,怎么办呢?为了解决这种问题,Rust发明了一个“强化了的普通魔镜”Cell<T>。如名字所示,它是一个容器,其内部包含的东西都可以在共享引用的状态下被修改,这种现象叫做“内部可变性”,Cell<T>实现了内部可变性。先看看Cell的API:

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);
  //     ^
  //     |
  //  魔法出现在这里
}

注意看,它的set方法中传入的是一个自身的共享引用。而通常情况下应该要传入可变引用(&mut self)。这就是魔法发生的地方。

应用示例

下面来看看如果使用Cell<T>来解决计数器问题:

struct Person {
  name: String,
  age: usize,
  count: Cell<usize>, // 一共有多少个副本,包含自身
}

impl Clone for Person {
    fn clone(&self) -> Self {
      let new_count = self.count.get() + 1;
      
      Person {
        name: self.name.clone(),
        age: self.age,
        count: Cell::new(new_count),
      }
      
      self.count.set(new_count);  // ok
    }
}

fn main() {
  let person = Person { name: "zengsai".to_string(), age: 18, count: Cell::new(1) };
  //    ^         ^
  //    |         |
  // 所有者     对象或值
  
  println!("Person count : {}", person.count.get());  // OUTPUT: Person count : 1
  
  let person_cloned_1: Person = person.clone();

  println!("Person count : {}", person.count.get());  // OUTPUT: Person count : 2
  
  let person_cloned_2: Person = &person;
 
  println!("Person count : {}", person.count.get());  // OUTPUT: Person count : 3
}

两者差异

当然,内部可变性违反了Rust关于共享引用的相关规定,但这是公安局备案的,Rust特许的,在标准库中只有两个容器可以这样做,一个是Cell<T>、还有一个是RefCell<T>,两者为了解决的问题是一样的,只是适用条件不同。

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

什么时候应该选择使用Cell,什么时候应该选择使用RefCell,两都差异待补充。

展开阅读全文
加载中
点击加入讨论🔥(3) 发布并加入讨论🔥
打赏
3 评论
0 收藏
3
分享
返回顶部
顶部