Rust Rc<T> 引用計(jì)數(shù)智能指針

2023-03-22 15:13 更新
ch15-04-rc.md
commit 45fe0fc9af98a214ed779d2cfac6773bdbfc708e

大部分情況下所有權(quán)是非常明確的:可以準(zhǔn)確地知道哪個(gè)變量擁有某個(gè)值。然而,有些情況單個(gè)值可能會(huì)有多個(gè)所有者。例如,在圖數(shù)據(jù)結(jié)構(gòu)中,多個(gè)邊可能指向相同的節(jié)點(diǎn),而這個(gè)節(jié)點(diǎn)從概念上講為所有指向它的邊所擁有。節(jié)點(diǎn)直到?jīng)]有任何邊指向它之前都不應(yīng)該被清理。

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

可以將其想象為客廳中的電視。當(dāng)一個(gè)人進(jìn)來看電視時(shí),他打開電視。其他人也可以進(jìn)來看電視。當(dāng)最后一個(gè)人離開房間時(shí),他關(guān)掉電視因?yàn)樗辉俦皇褂昧恕H绻橙嗽谄渌诉€在看的時(shí)候就關(guān)掉了電視,正在看電視的人肯定會(huì)抓狂的!

Rc<T> 用于當(dāng)我們希望在堆上分配一些內(nèi)存供程序的多個(gè)部分讀取,而且無法在編譯時(shí)確定程序的哪一部分會(huì)最后結(jié)束使用它的時(shí)候。如果確實(shí)知道哪部分是最后一個(gè)結(jié)束使用的話,就可以令其成為數(shù)據(jù)的所有者,正常的所有權(quán)規(guī)則就可以在編譯時(shí)生效。

注意 Rc<T> 只能用于單線程場(chǎng)景;第十六章并發(fā)會(huì)涉及到如何在多線程程序中進(jìn)行引用計(jì)數(shù)。

使用 Rc<T> 共享數(shù)據(jù)

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

trpl15-03

圖 15-3: 兩個(gè)列表, b 和 c, 共享第三個(gè)列表 a 的所有權(quán)

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

嘗試使用 Box<T> 定義的 List 實(shí)現(xiàn)并不能工作,如示例 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: 展示不能用兩個(gè) Box<T> 的列表嘗試共享第三個(gè)列表的所有權(quán)

編譯會(huì)得出如下錯(cuò)誤:

$ 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 成員擁有其儲(chǔ)存的數(shù)據(jù),所以當(dāng)創(chuàng)建 b 列表時(shí),a 被移動(dòng)進(jìn)了 b 這樣 b 就擁有了 a。接著當(dāng)再次嘗試使用 a 創(chuàng)建 c 時(shí),這不被允許,因?yàn)?nbsp;a 的所有權(quán)已經(jīng)被移動(dòng)。

可以改變 Cons 的定義來存放一個(gè)引用,不過接著必須指定生命周期參數(shù)。通過指定生命周期參數(shù),表明列表中的每一個(gè)元素都至少與列表本身存在的一樣久。這是示例 15-17 中元素與列表的情況,但并不是所有情況都如此。

相反,我們修改 List 的定義為使用 Rc<T> 代替 Box<T>,如列表 15-18 所示。現(xiàn)在每一個(gè) Cons 變量都包含一個(gè)值和一個(gè)指向 List 的 Rc<T>。當(dāng)創(chuàng)建 b 時(shí),不同于獲取 a 的所有權(quán),這里會(huì)克隆 a 所包含的 Rc<List>,這會(huì)將引用計(jì)數(shù)從 1 增加到 2 并允許 a 和 b 共享 Rc<List> 中數(shù)據(jù)的所有權(quán)。創(chuàng)建 c 時(shí)也會(huì)克隆 a,這會(huì)將引用計(jì)數(shù)從 2 增加為 3。每次調(diào)用 Rc::clone,Rc<List> 中數(shù)據(jù)的引用計(jì)數(shù)都會(huì)增加,直到有零個(gè)引用之前其數(shù)據(jù)都不會(huì)被清理。

文件名: 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> 引入作用域,因?yàn)樗辉?prelude 中。在 main 中創(chuàng)建了存放 5 和 10 的列表并將其存放在 a 的新的 Rc<List> 中。接著當(dāng)創(chuàng)建 b 和 c 時(shí),調(diào)用 Rc::clone 函數(shù)并傳遞 a 中 Rc<List> 的引用作為參數(shù)。

也可以調(diào)用 a.clone() 而不是 Rc::clone(&a),不過在這里 Rust 的習(xí)慣是使用 Rc::cloneRc::clone 的實(shí)現(xiàn)并不像大部分類型的 clone 實(shí)現(xiàn)那樣對(duì)所有數(shù)據(jù)進(jìn)行深拷貝。Rc::clone 只會(huì)增加引用計(jì)數(shù),這并不會(huì)花費(fèi)多少時(shí)間。深拷貝可能會(huì)花費(fèi)很長(zhǎng)時(shí)間。通過使用 Rc::clone 進(jìn)行引用計(jì)數(shù),可以明顯的區(qū)別深拷貝類的克隆和增加引用計(jì)數(shù)類的克隆。當(dāng)查找代碼中的性能問題時(shí),只需考慮深拷貝類的克隆而無需考慮 Rc::clone 調(diào)用。

克隆 Rc<T> 會(huì)增加引用計(jì)數(shù)

讓我們修改示例 15-18 的代碼以便觀察創(chuàng)建和丟棄 a 中 Rc<List> 的引用時(shí)引用計(jì)數(shù)的變化。

在示例 15-19 中,修改了 main 以便將列表 c 置于內(nèi)部作用域中,這樣就可以觀察當(dāng) c 離開作用域時(shí)引用計(jì)數(shù)如何變化。

文件名: 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));
}

示例 15-19:打印出引用計(jì)數(shù)

在程序中每個(gè)引用計(jì)數(shù)變化的點(diǎn),會(huì)打印出引用計(jì)數(shù),其值可以通過調(diào)用 Rc::strong_count 函數(shù)獲得。這個(gè)函數(shù)叫做 strong_count 而不是 count 是因?yàn)?nbsp;Rc<T> 也有 weak_count;在 “避免引用循環(huán):將 Rc<T> 變?yōu)?nbsp;Weak<T> 部分會(huì)講解 weak_count 的用途。

這段代碼會(huì)打印出:

$ 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> 的初始引用計(jì)數(shù)為1,接著每次調(diào)用 clone,計(jì)數(shù)會(huì)增加1。當(dāng) c 離開作用域時(shí),計(jì)數(shù)減1。不必像調(diào)用 Rc::clone 增加引用計(jì)數(shù)那樣調(diào)用一個(gè)函數(shù)來減少計(jì)數(shù);Drop trait 的實(shí)現(xiàn)當(dāng) Rc<T> 值離開作用域時(shí)自動(dòng)減少引用計(jì)數(shù)。

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

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


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)