大部分情況下所有權是非常明確的:可以準確地知道哪個變量擁有某個值。然而,有些情況單個值可能會有多個所有者。例如,在圖數據結構中,多個邊可能指向相同的節點,而這個節點從概念上講爲所有指向它的邊所擁有。節點在沒有任何邊指向它從而沒有任何所有者之前,都不應該被清理掉。
爲了啓用多所有權需要顯式地使用 Rust 類型 Rc<T>
,其爲 引用計數(reference counting)的縮寫。引用計數意味着記錄一個值的引用數量來知曉這個值是否仍在被使用。如果某個值有零個引用,就代表沒有任何有效引用並可以被清理。
可以將其想象爲客廳中的電視。當一個人進來看電視時,他打開電視。其他人也可以進來看電視。當最後一個人離開房間時,他關掉電視因爲它不再被使用了。如果某人在其他人還在看的時候就關掉了電視,正在看電視的人肯定會抓狂的!
Rc<T>
用於當我們希望在堆上分配一些內存供程序的多個部分讀取,而且無法在編譯時確定程序的哪一部分會最後結束使用它的時候。如果確實知道哪部分是最後一個結束使用的話,就可以令其成爲數據的所有者,正常的所有權規則就可以在編譯時生效。
注意 Rc<T>
只能用於單線程場景;第十六章併發會涉及到如何在多線程程序中進行引用計數。
使用 Rc<T>
共享數據
讓我們回到示例 15-5 中使用 Box<T>
定義 cons list 的例子。這一次,我們希望創建兩個共享第三個列表所有權的列表,其概念將會看起來如圖 15-3 所示:
列表 a
包含 5 之後是 10,之後是另兩個列表:b
從 3 開始而 c
從 4 開始。b
和 c
會接上包含 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)); }
編譯會得出如下錯誤:
$ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list) error[E0382]: use of moved value: `a` --> src/main.rs:11:30 | 9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil)))); | - move occurs because `a` has type `List`, which does not implement the `Copy` trait 10 | let b = Cons(3, Box::new(a)); | - value moved here 11 | let c = Cons(4, Box::new(a)); | ^ value used here after move For more information about this error, try `rustc --explain E0382`. error: could not compile `cons-list` due to previous error
Cons
成員擁有其儲存的數據,所以當創建 b
列表時,a
被移動進了 b
這樣 b
就擁有了 a
。接着當再次嘗試使用 a
創建 c
時,這不被允許,因爲 a
的所有權已經被移動。
可以改變 Cons
的定義來存放一個引用,不過接着必須指定生命週期參數。通過指定生命週期參數,表明列表中的每一個元素都至少與列表本身存在的一樣久。這是示例 15-17 中元素與列表的情況,但並不是所有情況都如此。
相反,我們修改 List
的定義爲使用 Rc<T>
代替 Box<T>
,如列表 15-18 所示。現在每一個 Cons
變量都包含一個值和一個指向 List
的 Rc<T>
。當創建 b
時,不同於獲取 a
的所有權,這裏會克隆 a
所包含的 Rc<List>
,這會將引用計數從 1 增加到 2 並允許 a
和 b
共享 Rc<List>
中數據的所有權。創建 c
時也會克隆 a
,這會將引用計數從 2 增加爲 3。每次調用 Rc::clone
,Rc<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)); }
需要使用 use
語句將 Rc<T>
引入作用域,因爲它不在 prelude 中。在 main
中創建了存放 5 和 10 的列表並將其存放在 a
的新的 Rc<List>
中。接着當創建 b
和 c
時,調用 Rc::clone
函數並傳遞 a
中 Rc<List>
的引用作爲參數。
也可以調用 a.clone()
而不是 Rc::clone(&a)
,不過在這裏 Rust 的習慣是使用 Rc::clone
。Rc::clone
的實現並不像大部分類型的 clone
實現那樣對所有數據進行深拷貝。Rc::clone
只會增加引用計數,這並不會花費多少時間。深拷貝可能會花費很長時間。通過使用 Rc::clone
進行引用計數,可以明顯的區別深拷貝類的克隆和增加引用計數類的克隆。當查找代碼中的性能問題時,只需考慮深拷貝類的克隆而無需考慮 Rc::clone
調用。
克隆 Rc<T>
會增加引用計數
讓我們修改示例 15-18 的代碼以便觀察創建和丟棄 a
中 Rc<List>
的引用時引用計數的變化。
在示例 15-19 中,修改了 main
以便將列表 c
置於內部作用域中,這樣就可以觀察當 c
離開作用域時引用計數如何變化。
文件名:src/main.rs
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)); }
在程序中每個引用計數變化的點,會打印出引用計數,其值可以通過調用 Rc::strong_count
函數獲得。這個函數叫做 strong_count
而不是 count
是因爲 Rc<T>
也有 weak_count
;在 “避免引用循環:將 Rc<T>
變爲 Weak<T>
” 部分會講解 weak_count
的用途。
這段代碼會打印出:
$ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list) Finished dev [unoptimized + debuginfo] target(s) in 0.45s Running `target/debug/cons-list` count after creating a = 1 count after creating b = 2 count after creating c = 3 count after c goes out of scope = 2
我們能夠看到 a
中 Rc<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>
結合使用來處理不可變性的限制。
from:Rc<T>
引用計數智能指針