0x00 开篇
在 Rust 中,始终遵守共享不可变,可变不共享的原则。对于可变的修改能力,有时可能只需要一点点就可以。有这样一种场景,我创建了一个结构体实例,但是我只想修改内部的数据,并不想将整个实例可变,那我们应该如何解决呢?其实,Rust 提供了这样一种能力,它允许变量在不可变引用前提下修改内部的数据,这就是 Rust 的内部可变性(Interior Mutability)。 本文将继续接上一节来介绍 Rust 的内部可变性。本篇文章的阅读时间大约 10 分钟。
0x01 RefCell
既然 Cell
无法获取非 Copy 类型的数据,那么我们来用另一种数据类型 RefCell
。先看源码:
结构体 RefCell
中,有三个字段,先来分别解释下:
borrow
: 它是Cell<BorrowFlag>
类型,再看第 12 行代码,BorrowFlag
其实是isize
类型的别名。它主要用于跟踪当前有多少个不可变引用。其实它就是一个计数器。borrowed_at
: 它是Cell<Option<&'static crate::panic::Location<'static>>>类型
我们一般无需关心该字段。该字段被标了feature = "debug_refcell"
,仅在启用了debug_refcell
特性的情况下,来记录当前最早发生的存活状态的借用(borrow)的位置。当我们调用borrow
或borrow_mut
方法时,如果borrow
计数器从 0 增加到 1,该字段将更新为借用操作发生的位置(通过panic::Location
类型来记录)Location
在这里不做介绍,大家如果感兴趣可以自行查阅源码。value
: 它是个UnsafeCell<T>
类型,用来存储RefCell<T>
管理的可变值,与上节介绍的Cell<T>
的value
相同。
直接看它们的定义可能有一点难理解,下面的示例会让我们更好的理解它们。
0x02 RefCell 常用方法
在 RefCell<T>
中,并没有同 Cell<T>
相同的 get
和 set
方法,而是通过 borrow
和 borrow_mut
来操作它们。
borrow 方法
获取 RefCell<T>
中 T
类型的不可变引用。返回类型是Ref<T>
,Ref<T>
也是一个智能指针,与 &
类似,表示对一个值的不可变引用。Ref<T>
主要用于获取 RefCell<T>
中包含的值的不可变引用。允许发生多次借用,但是如果在同一作用域中这个值已经被可变引用(borrow_mut)借用了,且可变引用生命期仍处于存活状态,则会发生错误。示例代码如下:
fn main() {
let name = String::from("ZhangSan");
let name_refcell = RefCell::new(name);
// borrow
let borrow1 = name_refcell.borrow();
let borrow2 = name_refcell.borrow();
println!("borrow1 => {}", borrow1);
println!("borrow2 => {}", borrow2);
}
// 运行结果
// borrow1 => ZhangSan
// borrow2 => ZhangSan
Rust 并没有提供可以直接查看 RefCell
借用状态的方法。我们通过调试+断点从图中也可以看到,当前的借用数量是 2。
borrow_mut 方法
获取 RefCell<T>
中 T
类型的可变引用。返回类型是RefMut<T>
,RefMut<T>
也是一个智能指针,与 &mut
类似,表示对一个值的可变引用。RefMut<T>
主要用于获取 RefCell<T>
中包含的值的可变引用。如果在同一作用域中这个值已经被借用(无论是_borrow_还是_borrow_mut_)借用了,且引用生命期仍处于存活状态,则会发生错误。示例代码如下:
示例代码如下:
fn main() {
let name = String::from("ZhangSan");
let name_refcell = RefCell::new(name);
println!("修改前: {:?}", name_refcell);
// 引用的生命期属于当前main作用域
let mut borrow_mut1 = name_refcell.borrow_mut();
borrow_mut1.push_str("Feng");
println!("修改后: {:?}", name_refcell);
// name_refcell.borrow_mut(); 该行代码会发生错误
// name_refcell.borrow(); 该行代码会发生错误
}
// 运行结果
// 修改前: RefCell { value: "ZhangSan" }
// 修改后: RefCell { value: <borrowed> }
上面的代码可以看到,我们通过 borrow_mut
获取 RefMut
,然后去修改字符串后,输出的结果是 <borrowed>
。这里是告诉你,该值的可变引用仍处于存活状态,也就是说 borrow_mut1
的生命期还没有结束。这里有以下几种解决方法:
在不同作用域中修改值
创建一个作用域,在作用域中修改值。示例代码如下:
fn main() {
let name = String::from("ZhangSan");
let name_refcell = RefCell::new(name);
println!("修改前: {:?}", name_refcell);
{
let mut borrow_mut1 = name_refcell.borrow_mut();
borrow_mut1.push_str("Feng");
}
println!("修改后: {:?}", name_refcell);
}
// 运行结果
// 修改前: RefCell { value: "ZhangSan" }
// 修改后: RefCell { value: "ZhangSanFeng" }
通过表达式的特性缩短借用的生命期
通过表达式的特性,缩短引用的生命期。我们将借用后的值不再绑定到变量上,而是直接在方法调用的结果上进行访问和修改,那么可变引用的生命期和表达式生命期的长度相同。示例代码如下:
fn main() {
let name = String::from("ZhangSan");
let name_refcell = RefCell::new(name);
println!("修改前: {:?}", name_refcell);
name_refcell.borrow_mut().push_str("Feng");
println!("修改后: {:?}", name_refcell);
}
// 运行结果
// 修改前: RefCell { value: "ZhangSan" }
// 修改后: RefCell { value: "ZhangSanFeng" }
手动回收引用
第 21 课中,我们了解过 std::mem::drop
,通过该函数,我们可以回收某个变量。
fn main() {
let name = String::from("ZhangSan");
let name_refcell = RefCell::new(name);
let mut borrow_mut1 = name_refcell.borrow_mut();
borrow_mut1.push_str("Feng");
std::mem::drop(borrow_mut1);
println!("修改后: {:?}", name_refcell);
}
// 运行结果
// 修改前: RefCell { value: "ZhangSan" }
// 修改后: RefCell { value: "ZhangSanFeng" }
借助共享所有权 Rc
前面第 12 课我们了解过共享所有权 Rc
通过它来包裹 RefCell
,表面是不可变的变量,其实内部是可以修改的。像 Rc<RefCell<T>>
这种类型,在用 Rust 实现某些数据结构时会很常用,如二叉树。
fn main() {
let name = String::from("ZhangSan");
let name_refcell = RefCell::new(name);
let rc = Rc::new(name_refcell);
rc.borrow_mut().push_str("Feng");
println!("修改后: {:?}", rc);
}
// 运行结果
// 修改前: RefCell { value: "ZhangSan" }
// 修改后: RefCell { value: "ZhangSanFeng" }
0x03 解决上节开篇的问题
最后贴一下上节的开篇问题的代码。
fn main() {
let id = 1;
let name = "ZhangSan";
let age = Cell::new(18);
let address = RefCell::new("北京".to_string());
let stu = Student { id, name, age, address };
println!("stu 修改前: {:?}", stu);
stu.age.set(20);
stu.address.borrow_mut().clear();
stu.address.borrow_mut().push_str("上海");
println!("stu 修改后: {:?}", stu);
}
/// 学生 结构体
#[derive(Debug)]
struct Student {
// 学号
id: u32,
// 姓名
name: &'static str,
// 年龄
age: Cell<u32>,
// 地址
address: RefCell<String>,
}
既保证了学号和姓名不可修改,又增加了可以修改年龄和地址的灵活性,一举两得。
0x04 小结
在工作中,使用 Cell
和 RefCell
会非常方便,但是感觉它有一点点儿违背了 Rust 的原则,哈哈。另外还有个缺点就是它们都是 线程不安全的。在多线程中保证安全需要使用 Mutex<T>
原子类型,我在后面的进阶教程中会介绍到它哦