Rust 中级教程 第22课——内部可变性(1)在 Rust 中,始终遵守**共享不可变,可变不共享**的原则。对于可变
0x00 开篇
在 Rust 中,始终遵守共享不可变,可变不共享的原则。对于可变的修改能力,有时可能只需要一点点就可以。有这样一种场景,我创建了一个结构体实例,但是我只想修改内部的数据,并不想将整个实例可变,那我们应该如何解决呢?其实,Rust 提供了这样一种能力,它允许变量在不可变引用前提下修改内部的数据,这就是 Rust 的内部可变性(Interior Mutability) 。本篇文章的阅读时间大约 10 分钟。
0x01 场景引入
在一个班级里有很多学生,有些学生的资料信息可能会发生更改,如年龄,家庭住址,我们需要及时更新这些信息。但是又有些信息又不想被修改,如学号。那我们应该如何做呢?
我们通常会这样做,示例代码如下:
#[derive(Debug)]
struct Student1 {
// 学号
id: u32,
// 姓名
name: &'static str,
// 年龄
age: u32,
// 地址
address: String,
}
fn main() {
// mut 创建一个 Student 实例
let mut stu1 = Student1 {
id: 1,
name: "ZhangSan",
age: 18,
address: "北京".to_string(),
};
stu1.age = 18;
stu1.address = "天津".to_string();
println!("stu1 = {:?}", stu1);
}
有一点可以肯定的是,这种写法完全没有问题。我们将 stu1
设置为可变的,添加 mut
关键字声明,就可以随意更改字段的值。但是并没有满足要求,我在可以修改地址的同时,学号也可以被修改。这是我们需要用到 Cell
。
0x02 Cell
Cell<T>
是包含一个 T
类型私有值的结构体。它的特点是不需要使用 mut 关键字声明,也可以设置私有字段的值,当然也可以获取这个值。来看下官方源码定义。
Cell
内部的 value
是一个 UnsafeCell
结构体, UnsafeCell
结构体的 value
是 T
类型, T
可以是一个已知大小的类型,也可以是未知大小的类型。所有通过 &UnsafeCell<T>
对内部值的访问都需要使用 unsafe
来包裹代码(在 unsafe
块中可以直接使用原始指针)。示例代码如下:
fn main() {
// 简单使用 Cell —— Copy类型 i32
let cell = Cell::new(6);
println!("cell 修改前:{}", cell.get());
cell.set(9);
println!("cell 修改后:{}", cell.get());
}
// 运行结果
cell 修改前:6
cell 修改后:9
上面我们并没有将 cell
用 mut
关键字声明,依然可以修改内部的值。当然 T
类型也可以是非Copy
类型,如:String
,但是你将无法获取该值。示例代码如下:
fn main() {
// 简单使用 Cell —— 非Copy类型 String
let cell = Cell::new(String::from("CELL"));
// 下面一行代码会发生错误
println!("cell 修改前:{}", cell.get());
cell.set(String::from("cell"));
// 下面一行代码会发生错误
println!("cell 修改后:{}", cell.get());
}
// 代码编译失败
0x03 为什么 Cell 可以修改内部私有字段?
是不是有点儿刷新认知了,这岂不是与我们之前讲的冲突了吗?究其原因,我们还要去看源码。
我们在调用 set
方法的时候,内部又再次调用了 replace
方法,在 replace
方法内部,调用了 mem::replace
函数。mem::replace
函数的主要作用是直接操作内存,用 src
的值替换掉 dest
的值,同时返回被替换掉的就值。这个过程并不会分配新的内存空间,而是直接在原始内存空间上进行修改。另外,在替换的过程中,如果多个线程同时替换该值,可能会发生数据竞争,这里也并没有对多线程进行处理,所以 Cell
是非线程安全的,只适用于单线程场景。
0x04 为什么 Cell 无法获取非Copy 类型的值
要解释这个问题,还要看源码。
可以看到,官方在实现 get
方法时,是直接取消原始指针的引用,且标注 T
类型必须是实现 Copy
的类。有没有办法来实现 非Copy类型
的内部修改能力呢?答案是肯定的,我们下一篇文章继续来探讨。
0x05 使用 Cell 来修改代码
回到最开始的代码,我们将 age
字段使用 Cell
来包装,代码如下:
/// 学生2 结构体
#[derive(Debug)]
struct Student2 {
// 学号
id: u32,
// 姓名
name: &'static str,
// 年龄
age: Cell<u32>,
// 地址
address: String,
}
fn main() {
// stu2 并不需要可变
let stu2 = Student2 {
id: 2,
name: "LiSi",
age: Cell::new(20),
address: String::from("上海"),
};
println!("stu2 修改年龄前: stu2 = {:?}", stu2);
stu2.age.set(22);
println!("stu2 修改年龄后: stu2 = {:?}", stu2);
}
// 运行结果
// stu2 修改年龄前: stu2 = Student2 { id: 2, name: "LiSi", age: Cell { value: 20 }, address: "上海" }
// stu2 修改年龄后: stu2 = Student2 { id: 2, name: "LiSi", age: Cell { value: 22 }, address: "上海" }
0x06 小结
在本篇文章之前,始终遵守共享不可变,可变不共享的原则。Cell
算是一种恰到好处的为违背不可修改规则提供了一种安全的方式。但是**Cell
是非线程安全的,只适用于单线程场景**。另外,Cell
也是智能指针的一种。
转载自:https://juejin.cn/post/7226187878320914493