likes
comments
collection
share

Rust 中级教程 第22课——内部可变性(1)在 Rust 中,始终遵守**共享不可变,可变不共享**的原则。对于可变

作者站长头像
站长
· 阅读数 28

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 关键字声明,也可以设置私有字段的值,当然也可以获取这个值。来看下官方源码定义。

Rust 中级教程 第22课——内部可变性(1)在 Rust 中,始终遵守**共享不可变,可变不共享**的原则。对于可变

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 可以修改内部私有字段?

是不是有点儿刷新认知了,这岂不是与我们之前讲的冲突了吗?究其原因,我们还要去看源码。

Rust 中级教程 第22课——内部可变性(1)在 Rust 中,始终遵守**共享不可变,可变不共享**的原则。对于可变

我们在调用 set 方法的时候,内部又再次调用了 replace 方法,在 replace 方法内部,调用了 mem::replace 函数。mem::replace 函数的主要作用是直接操作内存,用 src 的值替换掉 dest 的值,同时返回被替换掉的就值。这个过程并不会分配新的内存空间,而是直接在原始内存空间上进行修改。另外,在替换的过程中,如果多个线程同时替换该值,可能会发生数据竞争,这里也并没有对多线程进行处理,所以 Cell 是非线程安全的,只适用于单线程场景

0x04 为什么 Cell 无法获取非Copy 类型的值

要解释这个问题,还要看源码。

Rust 中级教程 第22课——内部可变性(1)在 Rust 中,始终遵守**共享不可变,可变不共享**的原则。对于可变

可以看到,官方在实现 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
评论
请登录