写点什么

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

用户头像
码生笔谈
关注
发布于: 4 小时前
5分钟速读之Rust权威指南(二十八)RefCell<T>

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

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

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

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


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


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

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

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




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


// 学生traitpub 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![] } }}
// 为男生实现学生的traitimpl 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>本身是不可变的,我们仍然能够更改其中存储的值。

发布于: 4 小时前阅读数: 2
用户头像

码生笔谈

关注

欢迎关注「码生笔谈」公众号 2018.09.09 加入

前端、Rust干货分享,一起成为更好的Javascripter & Rustacean

评论

发布
暂无评论
5分钟速读之Rust权威指南(二十八)RefCell<T>