为什么需要内部可变性

开发 前端
内部可变性提供了极大的灵活性,但是考滤到运行时开销,还是不能滥用,性能问题不大,重点是缺失了编译期的静态检查,会掩盖很多错误。

[[419570]]

本文参考 rust book ch15 并添加了自己的理解,感兴趣的可以先看看官方文档

Rust 有两种方式做到可变性

  • 继承可变性:比如一个 struct 声明时指定 let mut, 那么后续可以修改这个结构体的任一字段
  • 内部可变性:使用 Cell RefCell 包装变量或字段,这样即使外部的变量是只读的,也可以修改

看似继承可变性就够了,那么为什么还需要所谓的 interior mutability 内部可变性呢?让我们分析两个例子:

  1. struct Cache { 
  2.     x: i32, 
  3.     y: i32, 
  4.     sumOption<i32>, 
  5.  
  6. impl Cache { 
  7.     fn sum(&mut self) -> i32 { 
  8.         match self.sum { 
  9.             None => {self.sum=Some(self.x + self.y); self.sum.unwrap()}, 
  10.             Some(sum) => sum
  11.         } 
  12.     } 
  13.  
  14. fn main() { 
  15.     let i = Cache{x:10, y:11, sum: None}; 
  16.     println!("sum is {}", i.sum()); 

结构体 Cache 有三个字段,x, y, sum, 其中 sum 模拟 lazy init 懒加载的模式,上面代码是不能运行的,道理很简单,当 let 初始化变量 i 时,就是不可变的。

  1. 17 |     let i = Cache{x:10, y:11, sum: None}; 
  2.    |         - help: consider changing this to be mutable: `mut i` 
  3. 18 |     println!("sum is {}", i.sum()); 
  4.    |                           ^ cannot borrow as mutable 

有两种方式修复这个问题,let 声明时指定 let mut i, 但具体大的项目时,外层的变量很可能是 immutable 不可变的。这时内部可变性就派上用场了。

修复

  1. use std::cell::Cell; 
  2.  
  3. struct Cache { 
  4.     x: i32, 
  5.     y: i32, 
  6.     sum: Cell<Option<i32>>, 
  7.  
  8. impl Cache { 
  9.     fn sum(&self) -> i32 { 
  10.         match self.sum.get() { 
  11.             None => {self.sum.set(Some(self.x + self.y)); self.sum.get().unwrap()}, 
  12.             Some(sum) => sum
  13.         } 
  14.     } 
  15.  
  16. fn main() { 
  17.     let i = Cache{x:10, y:11, sum: Cell::new(None)}; 
  18.     println!("sum is {}", i.sum()); 

这是修复之后的代码,sum 类型是 Cell。

其实每一个都是有意义的,比如 Rc 代表共享所有权,但是因为 Rc 里的 T 要求是只读的,不能修改,所以就要用 Cell 封一层,这样就共享所有权,但还是可变的,Option 就是常见的要么有值 Some(T) 要么空值 None, 还是很好理解的。

如果不是写 rust 代码,只想阅读源码了解流程,没必要深究这些 wrapper, 重点关注包裹的真实类型就可以。

官网举的例子是 Mock Objects, 代码比较长,但是原理一样。

  1. struct MockMessenger { 
  2.       sent_messages: RefCell<Vec<String>>, 
  3.   } 

最后都是把结构体字段,使用 RefCell 包装一下。

Cell

  1. use std::cell::Cell; 
  2.  
  3. fn main(){ 
  4.     let a = Cell::new(1); 
  5.     let b = &a; 
  6.     a.set(1234); 
  7.     println!("b is {}", b.get()); 

这段代码非常有代表性,如果变量 a 没有用 Cell 包裹,那么在 b 只读借用存在的时间,是不允许修改 a 的,由 rust 编译器在 compile 编译期保证:给定一个对像,在作用域内(NLL)只允许存在 N 个不可变借用或者一个可变借用。

Cell 通过 get/set 来获取和修改值,这个函数要求 value 必须实现 Copy trait, 如果我们换成其它结构体,编译报错。

  1. error[E0599]: the method `get` exists for reference `&Cell<Test>`, but its trait bounds were not satisfied 
  2.   --> src/main.rs:11:27 
  3.    | 
  4. 3  | struct Test { 
  5.    | ----------- doesn't satisfy `Test: Copy` 
  6. ... 
  7. 11 |     println!("b is {}", b.get().a); 
  8.    |                           ^^^ 
  9.    | 
  10.    = note: the following trait bounds were not satisfied: 
  11.            `Test: Copy` 

从上面可以看到 struct Test 默认没有实现 Copy, 所以不允许使用 get. 那有没有办法获取底层 struct 呢?可以使用 get_mut 返回底层数据的引用,但这就要求整个变量是 let mut 的,所以与使用 Cell 的初衷不符,所以针对 Move 语义的场景,rust 提供了 RefCell。

RefCell

与 Cell 不一样,我们使用 RefCell 一般通过 borrow 获取不可变借用,或是 borrow_mut 获取底层数据的可变借用。

  1. use std::cell::{RefCell}; 
  2.  
  3. fn main() { 
  4.     let cell = RefCell::new(1); 
  5.  
  6.     let mut cell_ref_1 = cell.borrow_mut(); // Mutably borrow the underlying data 
  7.     *cell_ref_1 += 1; 
  8.     println!("RefCell value: {:?}", cell_ref_1); 
  9.  
  10.     let mut cell_ref_2 = cell.borrow_mut(); // Mutably borrow the data again (cell_ref_1 is still in scope though...) 
  11.     *cell_ref_2 += 1; 
  12.     println!("RefCell value: {:?}", cell_ref_2); 

代码来自 badboi.dev, 编译成功,但是运行失败。

  1. # cargo build 
  2.     Finished dev [unoptimized + debuginfo] target(s) in 0.03s 
  3. # cargo run 
  4.     Finished dev [unoptimized + debuginfo] target(s) in 0.03s 
  5.      Running `target/debug/hello_cargo` 
  6. RefCell value: 2 
  7. thread 'main' panicked at 'already borrowed: BorrowMutError', src/main.rs:10:31 
  8. note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace 

cell_ref_1 调用 borrow_mut 获取可变借用,还处于作用域时,cell_ref_2 也想获取可变借用,此时运行时检查报错,直接 panic。

也就是说 RefCell 将借用 borrow rule 由编译期 compile 移到了 runtime 运行时, 有一定的运行时开销。

  1. #[derive(Debug)] 
  2. enum List { 
  3.     Cons(Rc<RefCell<i32>>, Rc<List>), 
  4.     Nil, 
  5.  
  6. use crate::List::{Cons, Nil}; 
  7. use std::cell::RefCell; 
  8. use std::rc::Rc; 
  9.  
  10. fn main() { 
  11.     let value = Rc::new(RefCell::new(5)); 
  12.  
  13.     let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil))); 
  14.  
  15.     let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a)); 
  16.     let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a)); 
  17.  
  18.     *value.borrow_mut() += 10; 
  19.  
  20.     println!("a after = {:?}", a); 
  21.     println!("b after = {:?}", b); 
  22.     println!("c after = {:?}", c); 

这是官方例子,通过 Rc, RefCell 结合使用,做到共享所有权,同时又能修改 List 节点值。

小结 

内部可变性提供了极大的灵活性,但是考滤到运行时开销,还是不能滥用,性能问题不大,重点是缺失了编译期的静态检查,会掩盖很多错误。

 

责任编辑:武晓燕 来源: 董泽润的技术笔记
相关推荐

2022-07-14 23:27:57

数据分析数据驱动可变数据

2024-07-05 10:47:15

2023-10-30 23:38:03

Rust编程基础

2021-03-22 17:16:04

AI 数据人工智能

2015-03-19 15:04:06

2022-02-22 15:27:46

数据结构容器算法

2011-02-16 09:42:04

DevOps

2023-05-24 21:08:00

Linux发行版

2023-10-20 08:18:17

Python数据类型

2023-06-27 08:19:11

2017-09-26 09:50:18

2015-04-16 15:42:21

关系型数据库NoSQL

2022-06-28 14:54:26

加密货币数组货币安全

2015-10-12 08:56:27

Java不可变

2020-04-06 14:45:22

云计算边缘计算网络

2020-04-01 11:19:03

物联网LPWANIOT

2011-03-09 17:20:43

SSL VPNVPN

2022-12-01 14:43:56

物联网智慧城市

2021-03-03 08:32:43

集成晶振MCU

2023-09-05 09:49:03

点赞
收藏

51CTO技术栈公众号