rust语言基础学习:智能指针RefCell<T>与内部可变性模式

rust语言基础学习:智能指针RefCell<T>与内部可变性模式

2020-07-15
Rust

昨天学习了智能指针Rc<T>Rc<T>的使用场景是需要一个值有多个所有者的时候,这个场景出现在程序的运行时,Rc<T>是带有引用计数的智能指针,可以绕开编译器对所有权的静态检查。 Rc<T>内部使用了Box::leak将分配的堆内存泄露以绕开编译器对所有权规则的检查,泄露出来的堆内存不再受栈内存的自动管理,而是在运行时通过对引用计数的动态检查确保在合适的时机释放堆内存。

注意运行时的动态检查会牺牲一部分效率,通过对Rc<T>的学习,我们也能看出Rust对堆内存管理是从两个方向考虑的:

  • 通过编译时的静态检查确保我们编写的代码符合所有权规则,同时保证了效率和安全性,这适用于大多数场景
  • 通过运行时的动态检查,虽然会牺牲一部分效率,但这样可以灵活应对编译时的静态检查无法处理的一小部分特殊场景

今天我们学习智能指针RefCell<T>与内部可变性模式。

1.Rc是一个只读引用计数器 #

在学习RefCell<T>之前,先看一下能否通过Rc<T>修改堆上的数据。 我们已经知道可以用Rc.new()为某个类型T创建引用计数Rc,并将类型T的数据分配到堆上。 堆内存和栈内存的最大区别是:堆内存可以让在其上面动态创建的数据被四处使用。那么,能否在四处随意修改堆上的数据呢? 编写下面例1的代码验证一下。

例1:

 1use std::rc::Rc;
 2
 3#[derive(Debug)]
 4struct Employee {
 5    name: String,
 6}
 7
 8impl Employee {
 9    pub fn change_name(&mut self, name: String) {
10        self.name = name;
11    }
12}
13
14fn main() {
15    let employee = Rc::new(Employee {
16        name: String::from("张三"),
17    });
18    employee.change_name(String::from("张三十")); // cannot borrow data in an `Rc` as mutable. cannot borrow as mutable
19}

例1的代码无法编译通过,报了cannot borrow data in an Rc as mutable.错误。 这是因为Rc是一个只读的引用计数器,无法拿到Rc内部数据的可变引用来修改内部数据。

如果我们就是想修改堆上的数据呢?这就要使用RefCell了。这好像就和之前学习的借用规则"在同一时刻一个值已经创建了不可变引用,不能再为其创建可变引用"相矛盾。 借用规则是编译器的静态检查,这里RefCell肯定是要绕开借用规则的静态检查的,这就要了解一下Rust中的 内部可变性(interior mutability) 的概念。

2.内部可变性 #

以下是《The Rust Programming Language》中关于内部可变性的定义:

内部可变性(Interior mutability)是Rust中的一个设计模式,它允许你即使在有不可变引用时也可以改变数据,这通常是借用规则所不允许的。 为了改变数据,该模式在数据结构中使用unsafe代码(不安全代码)来模糊Rust通常的可变性和借用规则。 当可以确保代码在运行时会遵守借用规则,即使编译器不能保证的情况,可以选择使用那些运用内部可变性模式的类型。所涉及的unsafe代码将被封装进安全的API中,而外部类型仍然是不可变的。

直接从定义理解内部可变性还是比较晦涩难懂的。还是先来看外部可变性吧,外部可变性是使用mut显式声明的可变性,例如使用mut声明一个可变的值,使用&mut声明一个可变引用,编译器在编译时将对外部可用性进行静态检查。 但这样不够灵活,例1的场景就是需要在存在不可变引用的时候创建可变引用改变数据,这就需要跳过编译时对借用规则的静态检查,要有一种机制在编译时使编译器认为值是只读的,但在运行时可以得到这个值的可变引用来修改内部的数据。 这就是内部可变性。下面对例1代码进行修改,得到例2。

例2:

 1use std::cell::RefCell;
 2
 3#[derive(Debug)]
 4struct Employee {
 5    name: String,
 6}
 7
 8impl Employee {
 9    pub fn change_name(&mut self, name: String) {
10        self.name = name;
11    }
12}
13
14fn main() {
15    let employee = RefCell::new(Employee {
16        name: String::from("张三"),
17    });
18    {
19        let mut r = employee.borrow_mut(); // borrow_mut获得RefCell内部数据的可变引用
20        r.change_name(String::from("张三十"));
21    }
22    println!("{:?}", employee.borrow()); // borrow获得RefCell内部数据的不可变引用
23}

从例2中可以看到,employee是一个RefCell,通过borrow_mut方法获取可变引用,通过borrow方法获取不可变引用。使用{}将变量r的作用域提前结束,是因为虽然绕过了编译时对借用规则的静态检查,但在运行时还是会做动态检查,在运行时同一时刻如果存在可变引用和不可变引用,程序会panic。 内部可变性,是指的RefCell在运行时的内部可变性。

3.同时具有多所有者和内部可变性 #

接下来,再对例2的代码进一步修改,得到例3。

例3:

 1use std::cell::RefCell;
 2use std::rc::Rc;
 3
 4#[derive(Debug)]
 5struct Employee {
 6    name: String,
 7}
 8
 9impl Employee {
10    pub fn change_name(&mut self, name: String) {
11        self.name = name;
12    }
13}
14
15
16#[derive(Debug)]
17struct Company {
18    name: String,
19    manager: Rc<RefCell<Employee>>,
20}
21
22
23
24fn main() {
25    let employee = Rc::new(RefCell::new(Employee {
26        name: String::from("张三"),
27    }));
28  
29    let company1 = Company {
30        name: String::from("分公司1"),
31        manager: employee.clone(),
32    };
33    let company2 = Company {
34        name: String::from("分公司2"),
35        manager: employee.clone(),
36    };
37
38    employee.borrow_mut().change_name(String::from("张十三"));
39   
40    // Company { name: "分公司1", manager: RefCell { value: Employee { name: "张十三" } } }, Company { name: "分公司2", manager: RefCell { value: Employee { name: "张十三" } } }
41    println!("{:?}, {:?}", company1, company2);
42
43
44}

例3中通过Rc<RefCell<T>>的嵌套,允许T类型的值可以有多个所有者,又可以利用RefCell的内部可变性,来获取数据的可变引用。

4.内部可变性与外部可变性的区别 #

先看外部可变性的代码:

1
2fn main() {
3    let mut v = 5;
4    let r1 = &v;
5    let mut r2 = &mut v; // cannot borrow `v` as mutable because it is also borrowed as immutable
6    println!("{}", r1)
7}

这段代码是无法编译通过的,因为对于变量v,已经有了一个不可变引用r1,根据借用规则,在同一时刻不能再创建可变引用。可见,对于外部可变性,如果不符合规则就会产生编译错误。

再看内部可变性的示例代码:

1use std::cell::RefCell;
2
3fn main() {
4    let mut v = RefCell::new(5);
5    let r1 = v.borrow();
6    let mut r2 = v.borrow_mut();
7    println!("{}", r1)
8}

这段代码是可以编译通过的,但从这段代码看,不可变引用r1和可变引用r2在同一时刻同时存在,不符合借用规则,虽然可以编译通过,但在运行时会panic。

从这两段代码可以总结出内部可变性和外部可变性的主要区别如下:

创建方式借用规则的检查时机
内部可变性使用RefCell在运行时动态检查,如果不符合规则,将会panic
外部可变性let mut&mut创建在编译时静态检查,如果不符合规则,将会编译出错

5.总结 #

本节结合智能指针RefCell学习了Rust中的内部可变性,对比了Rust中的内部可变性和外部可变性的区别。 通过最近两节对RcRefCell的学习,我们可以看出Rust在解决内存管理问题时有以下2个"套路":

  • 对于绝大多数场景,通过编译时的静态检查,保证了效率和安全性。在编译时进行静态检查的优势是这些错误将在开发过程的早期被捕获,同时对运行时没有性能影响,因为所有的分析都提前完成了。
  • 对于小部分特殊场景,通过运行时的动态检查,虽然会牺牲一部分性能,但这样可以灵活应对编译时的静态检查无法处理的一小部分特殊场景

Rc允许值有多个所有者和RefCell的内部可变性模式都可以看到以上Rust处理问题的2个套路的体现。

参考 #

© 2024 青蛙小白