Rc<T> 引用計數智慧指針

ch15-04-rc.md
commit 6f292c8439927b4c5b870dd4afd2bfc52cc4eccc

大部分情況下所有權是非常明確的:可以準確地知道哪個變數擁有某個值。然而,有些情況單個值可能會有多個所有者。例如,在圖數據結構中,多個邊可能指向相同的節點,而這個節點從概念上講為所有指向它的邊所擁有。節點直到沒有任何邊指向它之前都不應該被清理。

為了啟用多所有權,Rust 有一個叫做 Rc<T> 的類型。其名稱為 引用計數reference counting)的縮寫。引用計數意味著記錄一個值引用的數量來知曉這個值是否仍在被使用。如果某個值有零個引用,就代表沒有任何有效引用並可以被清理。

可以將其想像為客廳中的電視。當一個人進來看電視時,他打開電視。其他人也可以進來看電視。當最後一個人離開房間時,他關掉電視因為它不再被使用了。如果某人在其他人還在看的時候就關掉了電視,正在看電視的人肯定會抓狂的!

Rc<T> 用於當我們希望在堆上分配一些記憶體供程序的多個部分讀取,而且無法在編譯時確定程序的哪一部分會最後結束使用它的時候。如果確實知道哪部分是最後一個結束使用的話,就可以令其成為數據的所有者,正常的所有權規則就可以在編譯時生效。

注意 Rc<T> 只能用於單執行緒場景;第十六章並發會涉及到如何在多執行緒程序中進行引用計數。

使用 Rc<T> 共享數據

讓我們回到範例 15-5 中使用 Box<T> 定義 cons list 的例子。這一次,我們希望創建兩個共享第三個列表所有權的列表,其概念將會看起來如圖 15-3 所示:

Two lists that share ownership of a third list

圖 15-3: 兩個列表, bc, 共享第三個列表 a 的所有權

列表 a 包含 5 之後是 10,之後是另兩個列表:b 從 3 開始而 c 從 4 開始。bc 會接上包含 5 和 10 的列表 a。換句話說,這兩個列表會嘗試共享第一個列表所包含的 5 和 10。

嘗試使用 Box<T> 定義的 List 實現並不能工作,如範例 15-17 所示:

檔案名: src/main.rs

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let a = Cons(5,
        Box::new(Cons(10,
            Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(4, Box::new(a));
}

範例 15-17: 展示不能用兩個 Box<T> 的列表嘗試共享第三個列表的所有權

編譯會得出如下錯誤:

error[E0382]: use of moved value: `a`
  --> src/main.rs:13:30
   |
12 |     let b = Cons(3, Box::new(a));
   |                              - value moved here
13 |     let c = Cons(4, Box::new(a));
   |                              ^ value used here after move
   |
   = note: move occurs because `a` has type `List`, which does not implement
   the `Copy` trait

Cons 成員擁有其儲存的數據,所以當創建 b 列表時,a 被移動進了 b 這樣 b 就擁有了 a。接著當再次嘗試使用 a 創建 c 時,這不被允許,因為 a 的所有權已經被移動。

可以改變 Cons 的定義來存放一個引用,不過接著必須指定生命週期參數。通過指定生命週期參數,表明列表中的每一個元素都至少與列表本身存在的一樣久。例如,借用檢查器不會允許 let a = Cons(10, &Nil); 編譯,因為臨時值 Nil 會在 a 獲取其引用之前就被丟棄了。

相反,我們修改 List 的定義為使用 Rc<T> 代替 Box<T>,如列表 15-18 所示。現在每一個 Cons 變數都包含一個值和一個指向 ListRc<T>。當創建 b 時,不同於獲取 a 的所有權,這裡會複製 a 所包含的 Rc<List>,這會將引用計數從 1 增加到 2 並允許 ab 共享 Rc<List> 中數據的所有權。創建 c 時也會複製 a,這會將引用計數從 2 增加為 3。每次調用 Rc::cloneRc<List> 中數據的引用計數都會增加,直到有零個引用之前其數據都不會被清理。

檔案名: src/main.rs

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}

範例 15-18: 使用 Rc<T> 定義的 List

需要使用 use 語句將 Rc<T> 引入作用域,因為它不在 prelude 中。在 main 中創建了存放 5 和 10 的列表並將其存放在 a 的新的 Rc<List> 中。接著當創建 bc 時,調用 Rc::clone 函數並傳遞 aRc<List> 的引用作為參數。

也可以調用 a.clone() 而不是 Rc::clone(&a),不過在這裡 Rust 的習慣是使用 Rc::cloneRc::clone 的實現並不像大部分類型的 clone 實現那樣對所有數據進行深拷貝。Rc::clone 只會增加引用計數,這並不會花費多少時間。深拷貝可能會花費很長時間。透過使用 Rc::clone 進行引用計數,可以明顯的區別深拷貝類的複製和增加引用計數類的複製體。當查找代碼中的性能問題時,只需考慮深拷貝類的複製而無需考慮 Rc::clone 調用。

複製 Rc<T> 會增加引用計數

讓我們修改範例 15-18 的代碼以便觀察創建和丟棄 aRc<List> 的引用時引用計數的變化。

在範例 15-19 中,修改了 main 以便將列表 c 置於內部作用域中,這樣就可以觀察當 c 離開作用域時引用計數如何變化。

檔案名: src/main.rs

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

範例 15-19:列印出引用計數

在程序中每個引用計數變化的點,會列印出引用計數,其值可以透過調用 Rc::strong_count 函數獲得。這個函數叫做 strong_count 而不是 count 是因為 Rc<T> 也有 weak_count;在 “避免引用循環:將 Rc<T> 變為 Weak<T> 部分會講解 weak_count 的用途。

這段代碼會列印出:

count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

我們能夠看到 aRc<List> 的初始引用計數為1,接著每次調用 clone,計數會增加1。當 c 離開作用域時,計數減1。不必像調用 Rc::clone 增加引用計數那樣調用一個函數來減少計數;Drop trait 的實現當 Rc<T> 值離開作用域時自動減少引用計數。

從這個例子我們所不能看到的是,在 main 的結尾當 b 然後是 a 離開作用域時,此處計數會是 0,同時 Rc<List> 被完全清理。使用 Rc<T> 允許一個值有多個所有者,引用計數則確保只要任何所有者依然存在其值也保持有效。

透過不可變引用, Rc<T> 允許在程序的多個部分之間只讀地共享數據。如果 Rc<T> 也允許多個可變引用,則會違反第四章討論的借用規則之一:相同位置的多個可變借用可能造成數據競爭和不一致。不過可以修改數據是非常有用的!在下一部分,我們將討論內部可變性模式和 RefCell<T> 類型,它可以與 Rc<T> 結合使用來處理不可變性的限制。