為什么需要內(nèi)部可變性
本文參考 rust book ch15 并添加了自己的理解,感興趣的可以先看看官方文檔
Rust 有兩種方式做到可變性
- 繼承可變性:比如一個(gè) struct 聲明時(shí)指定 let mut, 那么后續(xù)可以修改這個(gè)結(jié)構(gòu)體的任一字段
- 內(nèi)部可變性:使用 Cell RefCell 包裝變量或字段,這樣即使外部的變量是只讀的,也可以修改
看似繼承可變性就夠了,那么為什么還需要所謂的 interior mutability 內(nèi)部可變性呢?讓我們分析兩個(gè)例子:
- struct Cache {
- x: i32,
- y: i32,
- sum: Option<i32>,
- }
- impl Cache {
- fn sum(&mut self) -> i32 {
- match self.sum {
- None => {self.sum=Some(self.x + self.y); self.sum.unwrap()},
- Some(sum) => sum,
- }
- }
- }
- fn main() {
- let i = Cache{x:10, y:11, sum: None};
- println!("sum is {}", i.sum());
- }
結(jié)構(gòu)體 Cache 有三個(gè)字段,x, y, sum, 其中 sum 模擬 lazy init 懶加載的模式,上面代碼是不能運(yùn)行的,道理很簡單,當(dāng) let 初始化變量 i 時(shí),就是不可變的。
- 17 | let i = Cache{x:10, y:11, sum: None};
- | - help: consider changing this to be mutable: `mut i`
- 18 | println!("sum is {}", i.sum());
- | ^ cannot borrow as mutable
有兩種方式修復(fù)這個(gè)問題,let 聲明時(shí)指定 let mut i, 但具體大的項(xiàng)目時(shí),外層的變量很可能是 immutable 不可變的。這時(shí)內(nèi)部可變性就派上用場了。
修復(fù)
- use std::cell::Cell;
- struct Cache {
- x: i32,
- y: i32,
- sum: Cell<Option<i32>>,
- }
- impl Cache {
- fn sum(&self) -> i32 {
- match self.sum.get() {
- None => {self.sum.set(Some(self.x + self.y)); self.sum.get().unwrap()},
- Some(sum) => sum,
- }
- }
- }
- fn main() {
- let i = Cache{x:10, y:11, sum: Cell::new(None)};
- println!("sum is {}", i.sum());
- }
這是修復(fù)之后的代碼,sum 類型是 Cell。
其實(shí)每一個(gè)都是有意義的,比如 Rc 代表共享所有權(quán),但是因?yàn)?Rc
如果不是寫 rust 代碼,只想閱讀源碼了解流程,沒必要深究這些 wrapper, 重點(diǎn)關(guān)注包裹的真實(shí)類型就可以。
官網(wǎng)舉的例子是 Mock Objects, 代碼比較長,但是原理一樣。
- struct MockMessenger {
- sent_messages: RefCell<Vec<String>>,
- }
最后都是把結(jié)構(gòu)體字段,使用 RefCell 包裝一下。
Cell
- use std::cell::Cell;
- fn main(){
- let a = Cell::new(1);
- let b = &a;
- a.set(1234);
- println!("b is {}", b.get());
- }
這段代碼非常有代表性,如果變量 a 沒有用 Cell 包裹,那么在 b 只讀借用存在的時(shí)間,是不允許修改 a 的,由 rust 編譯器在 compile 編譯期保證:給定一個(gè)對(duì)像,在作用域內(nèi)(NLL)只允許存在 N 個(gè)不可變借用或者一個(gè)可變借用。
Cell 通過 get/set 來獲取和修改值,這個(gè)函數(shù)要求 value 必須實(shí)現(xiàn) Copy trait, 如果我們換成其它結(jié)構(gòu)體,編譯報(bào)錯(cuò)。
- error[E0599]: the method `get` exists for reference `&Cell<Test>`, but its trait bounds were not satisfied
- --> src/main.rs:11:27
- |
- 3 | struct Test {
- | ----------- doesn't satisfy `Test: Copy`
- ...
- 11 | println!("b is {}", b.get().a);
- | ^^^
- |
- = note: the following trait bounds were not satisfied:
- `Test: Copy`
從上面可以看到 struct Test 默認(rèn)沒有實(shí)現(xiàn) Copy, 所以不允許使用 get. 那有沒有辦法獲取底層 struct 呢?可以使用 get_mut 返回底層數(shù)據(jù)的引用,但這就要求整個(gè)變量是 let mut 的,所以與使用 Cell 的初衷不符,所以針對(duì) Move 語義的場景,rust 提供了 RefCell。
RefCell
與 Cell 不一樣,我們使用 RefCell 一般通過 borrow 獲取不可變借用,或是 borrow_mut 獲取底層數(shù)據(jù)的可變借用。
- use std::cell::{RefCell};
- fn main() {
- let cell = RefCell::new(1);
- let mut cell_ref_1 = cell.borrow_mut(); // Mutably borrow the underlying data
- *cell_ref_1 += 1;
- println!("RefCell value: {:?}", cell_ref_1);
- let mut cell_ref_2 = cell.borrow_mut(); // Mutably borrow the data again (cell_ref_1 is still in scope though...)
- *cell_ref_2 += 1;
- println!("RefCell value: {:?}", cell_ref_2);
- }
代碼來自 badboi.dev, 編譯成功,但是運(yùn)行失敗。
- # cargo build
- Finished dev [unoptimized + debuginfo] target(s) in 0.03s
- #
- # cargo run
- Finished dev [unoptimized + debuginfo] target(s) in 0.03s
- Running `target/debug/hello_cargo`
- RefCell value: 2
- thread 'main' panicked at 'already borrowed: BorrowMutError', src/main.rs:10:31
- note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
cell_ref_1 調(diào)用 borrow_mut 獲取可變借用,還處于作用域時(shí),cell_ref_2 也想獲取可變借用,此時(shí)運(yùn)行時(shí)檢查報(bào)錯(cuò),直接 panic。
也就是說 RefCell 將借用 borrow rule 由編譯期 compile 移到了 runtime 運(yùn)行時(shí), 有一定的運(yùn)行時(shí)開銷。
- #[derive(Debug)]
- enum List {
- Cons(Rc<RefCell<i32>>, Rc<List>),
- Nil,
- }
- use crate::List::{Cons, Nil};
- use std::cell::RefCell;
- use std::rc::Rc;
- fn main() {
- let value = Rc::new(RefCell::new(5));
- let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
- let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a));
- let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));
- *value.borrow_mut() += 10;
- println!("a after = {:?}", a);
- println!("b after = {:?}", b);
- println!("c after = {:?}", c);
- }
這是官方例子,通過 Rc, RefCell 結(jié)合使用,做到共享所有權(quán),同時(shí)又能修改 List 節(jié)點(diǎn)值。
小結(jié)
內(nèi)部可變性提供了極大的靈活性,但是考濾到運(yùn)行時(shí)開銷,還是不能濫用,性能問題不大,重點(diǎn)是缺失了編譯期的靜態(tài)檢查,會(huì)掩蓋很多錯(cuò)誤。