也來聊聊 Rust 的內部可變性

“內部可變性”的概念講起來很有迷惑性,理論知識可能很容易將新手繞暈。先來通俗說一下它的用途。其實這東西的產生,是因爲 Rust 的規矩太多,有着嚴格的內存訪問限制。內部可變性是一種變通手段,將 “破例” 的行爲仍然約束在 Rust 可控的範圍。就是說,你想破例也要按我的規則來破例。

我們都知道,Rust 的內存安全基於以下規則:給定對象 T,只能有以下情況之一:

  1. 對該對象有多個不可變引用(&T)(也稱爲別名)。

  2. 對該對象有一個可變引用(&mut T)(也稱爲可變性)。

這一規則由 Rust 編譯器強制執行。然而,有些情況下該規則並不夠靈活。有時會需要多個引用指向一個對象,並能同時對其進行修改。而 Rust 提供的 可共享的可變容器(Shareable mutable containers) 則允許以一種受控的方式實現可變性,即使已經存在別名。

這些容器包括:Cell<T>RefCell<T> 等,它們允許以單線程方式進行這樣的操作;而多個線程之間進行不可變引用引用並修改時,則可以使用 Mutex<T> 等。

事實上 Mutex 不過是一種實現 “多個引用指向一個對象,並能同時對其進行修改” 這一目的的方法而已。它爲內部數據提供獨佔(mut)訪問權限,儘管許多線程可能對 Mutex 本身具有共享(非 mut)訪問權限。

總結一下,內部可變性是一種允許在擁有不可變引用的同時修改數據的概念。這種能力是通過特定類型的容器實現的。這些容器允許在不違反借用規則的情況下,在擁有不可變引用的同時對數據進行修改。

如果還不懂,咱們來詳細說一下。

首先,既然有內部可變性,就有對應的外部可變性。 外部可變性一言以蔽之就是:變量的可變性直接繼承自包含該變量的容器。其實從其英文名 inherited mutability 我們可以感知到這點。

比方說有容器(此處爲結構體)Book :

struct Book {
    name: String,
}

如果想爲其增加一個方法,用來更改其 name

impl Book {
    fn change_name(&self, new_name: String) {
        self.name = new_name;
    }
}

然後你會發現編譯錯誤:

cannot assign to `self.name`, which is behind a `&` reference

這是因爲該類型需要從父級傳遞 &mut 訪問權限到子級,或者說是從容器到內容。只有當你擁有對容器 self 的 &mut 引用時,纔會預期能夠調用內容 self.name 併爲其賦值。

impl Book {
    fn change_name(&mut self, new_name: String) {
        self.name = new_name;
    }
}

同樣的,下面的代碼也將無法編譯:

let a = Book {
    name: String::from("老子"),
};
a.change_name(String::from("莊子"));

需要將 a 改爲 mut a 方可編譯。這是默認行爲,因爲如果你沒有對父級的獨佔訪問權限,Rust 通常無法確保你對子級有獨佔訪問權限。

內部可變性不同於 Rust 默認的 “持有共享引用不能改變” 或“持有可變引用不能還有共享引用”的非此即彼的編譯期約束。它更應該叫共享可變性——持有共享引用還可以改變數據。

現在來看 Rust 爲 “破例” 而設立的規則。其實就是用特定的容器來包裹你想要 “破例” 的內容,以便編譯器能識別到它們。這類容器典型的有 Cell<T> 與 RefCell<T>。而關於 Cell<T> 與 RefCell<T> 的區別,其實你只要記清:Cell<T> 適用於實現 Copy trait 的類型,而 RefCell<T> 適用於非 Copy 類型即可。

我們來爲 Book 增加點內容:

struct Book {
    name: String,
    price: Cell<i32>,
    author: RefCell<String>,
}

price 爲 i32 類型的,實現了 Copy ,所以我們用 Cell 包裹,相應的,String 類型的 author 則用了 RefCell 包裹。

再增加兩個方法:

fn change_price(&self, new_price: i32) {
    self.price.set(new_price);
}
fn change_author(&self, new_author: String) {
    let mut a = self.author.borrow_mut();
    *a = new_author;
}

我們注意到,此時不再需要 &mut self。調用的時候:

let a = Book {
    name: String::from("老子"),
    price: Cell::new(25),
    author: RefCell::new(String::from("老子")),
};

a.change_author(String::from("李耳"));
let b = &a;
b.change_price(30);
a.change_price(21);

而如果你嘗試以下代碼,理所當然會出現編譯錯誤:

a.change_name(String::from("莊子"));

這樣一來,我們的破例行爲又符合 Rust 的規則了。而明白了這一點,再去從 API 文檔中詳細學習 Cell<T> 、RefCell<T> 或是其它此類可共享的可變容器時,你便會有的放矢了。

最後我們把 author 打印出來看一下:

println!("{:?}", a.author.borrow());  // "李耳"

沒有問題。但如果我們嘗試把 change_author 方法中的代碼 copy 到 main 中來:

let a = Book { ... };
...

let mut d = a.author.borrow_mut();
*d = String::from("老聃");

println!("{:?}", a.author.borrow());

編譯沒問題,運行時卻會報錯:

然後我明確地進行 drop(d) 操作後,就又正常了:

let mut d = a.author.borrow_mut();
*d = String::from("老聃");
drop(d);

println!("{:?}", a.author.borrow());

想想看爲什麼?

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/iGG7Fl_8o11g9lUv7utvwA