likes
comments
collection
share

Rust 中级教程 第23课——内部可变性(2)

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

 0x00 开篇

在 Rust 中,始终遵守共享不可变,可变不共享的原则。对于可变的修改能力,有时可能只需要一点点就可以。有这样一种场景,我创建了一个结构体实例,但是我只想修改内部的数据,并不想将整个实例可变,那我们应该如何解决呢?其实,Rust 提供了这样一种能力,它允许变量在不可变引用前提下修改内部的数据,这就是 Rust 的内部可变性(Interior Mutability)。 本文将继续接上一节来介绍 Rust 的内部可变性。本篇文章的阅读时间大约 10 分钟

 0x01 RefCell

既然 Cell 无法获取非 Copy 类型的数据,那么我们来用另一种数据类型 RefCell。先看源码:

Rust 中级教程 第23课——内部可变性(2)

结构体 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 中级教程 第23课——内部可变性(2)

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> 原子类型,我在后面的进阶教程中会介绍到它哦

转载自:https://juejin.cn/post/7226651307523440699
评论
请登录