5分钟速读之Rust权威指南(二十八)RefCell<T>

1,199 阅读5分钟

RefCell<T>和内部可变性模式

上一节介绍了Rc<T>,对数据进行计数方式的引用,但是引用是不可变的,本节介绍的RefCell<T>引用则具有内部可变性(interior mutability),它允许我们在只持有不可变引用的前提下对数据进行修改。

内部可变性:可变地借用一个不可变的值

正常我们以可变引用来对一个不可变进行引用是编译无法通过的:

let x = 5;
let y = &mut x; // 报错,不能借用不可变变量作为可变变量

但是我们有时需要一个值在对外保持不可变性的同时能够在方法内部修改自身。除了这个值本身的方法,其余的代码则依然不能修改这个值。 使用RefCell<T>就是获得这种内部可变性的一种方法。

RefCell<T>并没有完全绕开借用规则

我们虽然使用内部可变性通过了编译阶段的借用检查,但借用检查的工作仅仅是被延后到了运行阶段,如果违反了借用规则,还是会触发panic!。


例如我们要实现一个观察者模式,老师可以向所有学生发送通知:

// 学生trait
pub trait Student {
  // 用于接收老师消息,注意这里的&self是不可变引用
  fn on_message(&self, msg: &str);
}

// 老师结构体
pub struct Teacher<'a, T: Student> {
  // 存放老师关注的学生
  students: Vec<&'a T>,
}

// 为老师实现一些方法
impl<'a, T: Student> Teacher<'a, T> {
  // 创建一个老师
  pub fn new() -> Teacher<'a, T> {
    Teacher {
      students: vec![],
    }
  }

  // 关注一个学生
  pub fn attach(&mut self, student: &'a T) {
    self.students.push(student)
  }

  // 通知所有学生
  pub fn notify(&self, msg: &str) {
    for student in self.students.iter() {
      // 调用所有学生的on_message方法
      student.on_message(msg)
    }
  }
}

根据上面的定义来实现业务逻辑:

// 首先定义一个男生类型
struct Boy {
  // 用于记录老师发过的消息
  messages: Vec<String>,
}

impl Boy {
  // 实现一个创建男生的方法
  fn new() -> Boy {
    Boy {
      messages: vec![]
    }
  }
}

// 为男生实现学生的trait
impl Student for Boy {
  fn on_message(&self, message: &str) {
    // 接受到老师的消息后存起来
    self.messages.push(String::from(message)); // 报错,不能够把self.messages作为可变引用,因为&self是不可变引用
	}
}

// 创建一个老师
let mut teacher = Teacher::new();
// 创建一个学生
let student = Boy::new();
// 老师关注这个学生
teacher.attach(&student);
// 老师通知所有学生
teacher.notify("下课");

println!("学生收到消息数量:{}", student.messages.len());

上边报错了,因为self.messages需要可变引用的self,那么我们尝试把on_message的参数&self改一下:

fn on_message(&mut self, message: &str) { // 改成引用类型,报错,on_message方法拥有不兼容的类型,因为student trait中的签名标识了&self是不可变的
  self.messages.push(String::from(message));
}

因为在Student trait的on_message签名中定义了self是不可变引用,上边的改动与签名并不兼容。

使用RefCell<T>实现内部可变

将上面的代码使用RefCell<T>来更改Boy的定义:

use std::cell::RefCell; // 从标准库中引入

struct Boy {
  messages: RefCell<Vec<String>>, // 更改messages的类型
}

impl Boy {
  fn new() -> Boy {
    Boy {
      messages: RefCell::new(vec![])  // 将vec保存在RefCell中
    }
  }
}

impl Student for Boy {
  fn on_message(&self, message: &str) { // self仍然是不可变引用
    // 在运行时借用 可变引用类型的messages
    self.messages.borrow_mut().push(String::from(message));
	}
}

let mut teacher = Teacher::new();
let student = Boy::new();
teacher.attach(&student);
teacher.notify("下课");

// 这里获取内部数组的长度时,需要借用
println!("学生收到消息数量:{}", student.messages.borrow().len()); // 1

使用RefCell<T>在运行时记录借用信息

在创建不可变和可变引用时分别使用语法&与&mut。对于RefCell<T>而言,需要使用borrow与borrow_mut方法来实现类似的功能。

RefCell<T>同样遵守借用规则

RefCell<T>会基于这一技术来维护和编译器同样的借用检查规则:在任何一个给定的时间里,它只允许你拥有多个不可变借用或一个可变借用。

fn on_message(&self, message: &str) {
  self.messages.borrow_mut().push(String::from(message));
  
  // panic! 不能够多次借用可变引用
  self.messages.borrow_mut().push(String::from(message));
}

将Rc<T>和RefCell<T>结合使用来实现一个拥有多重所有权的可变数据

我们有了具有多重不可变引用的Rc<T>和可变内部引用的RefCell<T>之后,就可以组合出多重引用的可变数据类型:

#[derive(Debug)]
enum List {
  Cons(Rc<RefCell<i32>>, Rc<List>),
  Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;
use std::cell::RefCell;

fn main() {
  // 使用Rc来包裹一个内部可变的值5
  let value = Rc::new(RefCell::new(5));
  // a节点引用value
  let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
  // b节点引用a节点
  let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));
  // c节点引用a节点
  let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));

  // 可变借用并改变value的值
  // 这里使用了自动解引用功能,来将Rc<T>解引用为RefCell<T>。
  *value.borrow_mut() += 10;

  println!("a after = {:?}", a);
  // a after = Cons(RefCell { value: 15 }, Nil)
  println!("b after = {:?}", b);
  // b after = Cons(RefCell { value: 6 }, Cons(RefCell { value: 15 }, Nil))
  println!("c after = {:?}", c);
  // c after = Cons(RefCell { value: 10 }, Cons(RefCell { value: 15 }, Nil))
}

上边显示改变value之后,abc中的value也全都改变了。

Box<T>、Rc<T>、RefCell<T>之间的区别:

  • Rc<T>允许一份数据有多个所有者,而Box<T>和RefCell<T>都只有一个所有者。
  • Box<T>允许在编译时检查的可变或不可变借用,Rc<T>仅允许编译时检查的不可变借用,RefCell<T>允许运行时检查的可变或不可变借用。
  • 由于RefCell<T>允许我们在运行时检查可变借用,所以即便RefCell<T>本身是不可变的,我们仍然能够更改其中存储的值。