指針?(pointer)是一個包含內(nèi)存地址的變量的通用概念。這個地址引用,或 “指向”(points at)一些其他數(shù)據(jù)。Rust 中最常見的指針是第四章介紹的?引用(reference)。引用以?&
?符號為標(biāo)志并借用了他們所指向的值。除了引用數(shù)據(jù)沒有任何其他特殊功能。它們也沒有任何額外開銷,所以應(yīng)用的最多。
另一方面,智能指針(smart pointers)是一類數(shù)據(jù)結(jié)構(gòu),他們的表現(xiàn)類似指針,但是也擁有額外的元數(shù)據(jù)和功能。智能指針的概念并不為 Rust 所獨有;其起源于 C++ 并存在于其他語言中。Rust 標(biāo)準(zhǔn)庫中不同的智能指針提供了多于引用的額外功能。本章將會探索的一個例子便是?引用計數(shù)?(reference counting)智能指針類型,其允許數(shù)據(jù)有多個所有者。引用計數(shù)智能指針記錄總共有多少個所有者,并當(dāng)沒有任何所有者時負(fù)責(zé)清理數(shù)據(jù)。
在 Rust 中,普通引用和智能指針的一個額外的區(qū)別是引用是一類只借用數(shù)據(jù)的指針;相反,在大部分情況下,智能指針?擁有?他們指向的數(shù)據(jù)。
智能指針通常使用結(jié)構(gòu)體實現(xiàn)。智能指針區(qū)別于常規(guī)結(jié)構(gòu)體的顯著特性在于其實現(xiàn)了?Deref
?和?Drop
?trait。Deref
?trait 允許智能指針結(jié)構(gòu)體實例表現(xiàn)的像引用一樣,這樣就可以編寫既用于引用、又用于智能指針的代碼。Drop
?trait 允許我們自定義當(dāng)智能指針離開作用域時運行的代碼。本章會討論這些 trait 以及為什么對于智能指針來說他們很重要。
考慮到智能指針是一個在 Rust 經(jīng)常被使用的通用設(shè)計模式,本章并不會覆蓋所有現(xiàn)存的智能指針。很多庫都有自己的智能指針而你也可以編寫屬于你自己的智能指針。這里將會講到的是來自標(biāo)準(zhǔn)庫中最常用的一些:
-
Box<T>
,用于在堆上分配值 -
Rc<T>
,一個引用計數(shù)類型,其數(shù)據(jù)可以有多個所有者 -
Ref<T>
?和?RefMut<T>
,通過?RefCell<T>
?訪問,一個在運行時而不是在編譯時執(zhí)行借用規(guī)則的類型。
另外我們會涉及?內(nèi)部可變性(interior mutability)模式,這時不可變類型暴露出改變其內(nèi)部值的 API。我們也會討論?引用循環(huán)(reference cycles)會如何泄露內(nèi)存,以及如何避免。
15.1?使用Box<T>指向堆上的數(shù)據(jù)
最簡單直接的智能指針是?box,其類型是?Box<T>
。 box 允許你將一個值放在堆上而不是棧上。留在棧上的則是指向堆數(shù)據(jù)的指針。
除了數(shù)據(jù)被儲存在堆上而不是棧上之外,box 沒有性能損失。不過也沒有很多額外的功能。它們多用于如下場景:
- 當(dāng)有一個在編譯時未知大小的類型,而又想要在需要確切大小的上下文中使用這個類型值的時候
- 當(dāng)有大量數(shù)據(jù)并希望在確保數(shù)據(jù)不被拷貝的情況下轉(zhuǎn)移所有權(quán)的時候
- 當(dāng)希望擁有一個值并只關(guān)心它的類型是否實現(xiàn)了特定 trait 而不是其具體類型的時候
我們會在 “box 允許創(chuàng)建遞歸類型” 部分展示第一種場景。在第二種情況中,轉(zhuǎn)移大量數(shù)據(jù)的所有權(quán)可能會花費很長的時間,因為數(shù)據(jù)在棧上進(jìn)行了拷貝。為了改善這種情況下的性能,可以通過 box 將這些數(shù)據(jù)儲存在堆上。接著,只有少量的指針數(shù)據(jù)在棧上被拷貝。第三種情況被稱為?trait 對象(trait object),第十七章剛好有一整個部分 “為使用不同類型的值而設(shè)計的 trait 對象” 專門講解這個主題。所以這里所學(xué)的內(nèi)容會在第十七章再次用上!
使用Box<T>在堆上儲存數(shù)據(jù)
在討論?Box<T>
?的用例之前,讓我們熟悉一下語法以及如何與儲存在?Box<T>
?中的值進(jìn)行交互。
fn main() {
let b = Box::new(5);
println!("b = {}", b);
}
這里定義了變量?b
,其值是一個指向被分配在堆上的值?5
?的?Box
。這個程序會打印出?b = 5
;在這個例子中,我們可以像數(shù)據(jù)是儲存在棧上的那樣訪問 box 中的數(shù)據(jù)。正如任何擁有數(shù)據(jù)所有權(quán)的值那樣,當(dāng)像?b
?這樣的 box 在?main
?的末尾離開作用域時,它將被釋放。這個釋放過程作用于 box 本身(位于棧上)和它所指向的數(shù)據(jù)(位于堆上)。
將一個單獨的值存放在堆上并不是很有意義。將像單個?i32
?這樣的值儲存在棧上,也就是其默認(rèn)存放的地方在大部分使用場景中更為合適。讓我們看看一個不使用 box 時無法定義的類型的例子。
Box允許創(chuàng)建遞歸類型
Rust 需要在編譯時知道類型占用多少空間。一種無法在編譯時知道大小的類型是?遞歸類型(recursive type),其值的一部分可以是相同類型的另一個值。這種值的嵌套理論上可以無限的進(jìn)行下去,所以 Rust 不知道遞歸類型需要多少空間。不過 box 有一個已知的大小,所以通過在循環(huán)類型定義中插入 box,就可以創(chuàng)建遞歸類型了。
讓我們探索一下?cons list,一個函數(shù)式編程語言中的常見類型,來展示這個(遞歸類型)概念。除了遞歸之外,我們將要定義的 cons list 類型是很直白的,所以這個例子中的概念,在任何遇到更為復(fù)雜的涉及到遞歸類型的場景時都很實用。
cons list 的更多內(nèi)容
cons list?是一個來源于 Lisp 編程語言及其方言的數(shù)據(jù)結(jié)構(gòu)。在 Lisp 中,cons
?函數(shù)(“construct function" 的縮寫)利用兩個參數(shù)來構(gòu)造一個新的列表,他們通常是一個單獨的值和另一個列表。
cons 函數(shù)的概念涉及到更常見的函數(shù)式編程術(shù)語;“將?x?與?y?連接” 通常意味著構(gòu)建一個新的容器而將?x?的元素放在新容器的開頭,其后則是容器?y?的元素。
cons list 的每一項都包含兩個元素:當(dāng)前項的值和下一項。其最后一項值包含一個叫做?Nil
?的值且沒有下一項。cons list 通過遞歸調(diào)用?cons
?函數(shù)產(chǎn)生。代表遞歸的終止條件(base case)的規(guī)范名稱是?Nil
,它宣布列表的終止。注意這不同于第六章中的 “null” 或 “nil” 的概念,他們代表無效或缺失的值。
注意雖然函數(shù)式編程語言經(jīng)常使用 cons list,但是它并不是一個 Rust 中常見的類型。大部分在 Rust 中需要列表的時候,Vec<T>
?是一個更好的選擇。其他更為復(fù)雜的遞歸數(shù)據(jù)類型?確實?在 Rust 的很多場景中很有用,不過通過以 cons list 作為開始,我們可以探索如何使用 box 毫不費力的定義一個遞歸數(shù)據(jù)類型。
示例包含一個 cons list 的枚舉定義。注意這還不能編譯因為這個類型沒有已知的大小,之后我們會展示:
enum List {
Cons(i32, List),
Nil,
}
注意:出于示例的需要我們選擇實現(xiàn)一個只存放?
i32
?值的 cons list。也可以用泛型,正如第十章講到的,來定義一個可以存放任何類型值的 cons list 類型。
使用這個 cons list 來儲存列表?1, 2, 3
?將看起來如示例
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
第一個?Cons
?儲存了?1
?和另一個?List
?值。這個?List
?是另一個包含?2
?的?Cons
?值和下一個?List
?值。接著又有另一個存放了?3
?的?Cons
?值和最后一個值為?Nil
?的?List
,非遞歸成員代表了列表的結(jié)尾。
套娃
結(jié)果
這個錯誤表明這個類型 “有無限的大小”。其原因是?List
?的一個成員被定義為是遞歸的:它直接存放了另一個相同類型的值。這意味著 Rust 無法計算為了存放?List
?值到底需要多少空間。讓我們一點一點來看:首先了解一下 Rust 如何決定需要多少空間來存放一個非遞歸類型。
計算非遞歸類型的大小
回憶一下第六章討論枚舉定義時示例中定義的?Message
?枚舉:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
當(dāng) Rust 需要知道要為?Message
?值分配多少空間時,它可以檢查每一個成員并發(fā)現(xiàn)?Message::Quit
?并不需要任何空間,Message::Move
?需要足夠儲存兩個?i32
?值的空間,依此類推。因此,Message
?值所需的空間等于儲存其最大成員的空間大小。
與此相對當(dāng) Rust 編譯器檢查像示例中的?List
?這樣的遞歸類型時會發(fā)生什么呢。編譯器嘗試計算出儲存一個?List
?枚舉需要多少內(nèi)存,并開始檢查?Cons
?成員,那么?Cons
?需要的空間等于?i32
?的大小加上?List
?的大小。為了計算?List
?需要多少內(nèi)存,它檢查其成員,從?Cons
?成員開始。Cons
成員儲存了一個?i32
?值和一個List
值,這樣的計算將無限進(jìn)行下去
使用Box<T>給遞歸類型一個已知的大小
Rust 無法計算出要為定義為遞歸的類型分配多少空間,所以編譯器給出了示例 中的錯誤。這個錯誤也包括了有用的建議:
= help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to
make `List` representable
在建議中,“indirection” 意味著不同于直接儲存一個值,我們將間接的儲存一個指向值的指針。
因為?Box<T>
?是一個指針,我們總是知道它需要多少空間:指針的大小并不會根據(jù)其指向的數(shù)據(jù)量而改變。這意味著可以將?Box
?放入?Cons
?成員中而不是直接存放另一個?List
?值。Box
?會指向另一個位于堆上的?List
?值,而不是存放在?Cons
?成員中。從概念上講,我們?nèi)匀挥幸粋€通過在其中 “存放” 其他列表創(chuàng)建的列表,不過現(xiàn)在實現(xiàn)這個概念的方式更像是一個項挨著另一項,而不是一項包含另一項。
fn main() {
let list = Cons(1,
Box::new(Cons(2,
Box::new(Cons(3,
Box::new(Nil))))));
}
Cons
?成員將會需要一個?i32
?的大小加上儲存 box 指針數(shù)據(jù)的空間。Nil
?成員不儲存值,所以它比?Cons
?成員需要更少的空間。現(xiàn)在我們知道了任何?List
?值最多需要一個?i32
?加上 box 指針數(shù)據(jù)的大小。通過使用 box ,打破了這無限遞歸的連鎖,這樣編譯器就能夠計算出儲存?List
?值需要的大小了。
box 只提供了間接存儲和堆分配;他們并沒有任何其他特殊的功能,比如我們將會見到的其他智能指針。它們也沒有這些特殊功能帶來的性能損失,所以他們可以用于像 cons list 這樣間接存儲是唯一所需功能的場景。
Box<T>
?類型是一個智能指針,因為它實現(xiàn)了?Deref
?trait,它允許?Box<T>
?值被當(dāng)作引用對待。當(dāng)?Box<T>
?值離開作用域時,由于?Box<T>
?類型?Drop
?trait 的實現(xiàn),box 所指向的堆數(shù)據(jù)也會被清除。
15.2?通過Deref trait將智能指針當(dāng)作常規(guī)引用處理
實現(xiàn)?Deref
?trait 允許我們重載?解引用運算符(dereference operator)*
(與乘法運算符或通配符相區(qū)別)。通過這種方式實現(xiàn)?Deref
?trait 的智能指針可以被當(dāng)作常規(guī)引用來對待,可以編寫操作引用的代碼并用于智能指針。
讓我們首先看看解引用運算符如何處理常規(guī)引用,接著嘗試定義我們自己的類似?Box<T>
?的類型并看看為何解引用運算符不能像引用一樣工作。
通過解引用運算符追蹤指針的值
常規(guī)引用是一個指針類型,一種理解指針的方式是將其看成指向儲存在其他某處值的箭頭。
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y); // 解引用
}
變量?x
?存放了一個?i32
?值?5
。y
?等于?x
?的一個引用。可以斷言?x
?等于?5
。然而,如果希望對?y
?的值做出斷言,必須使用?*y
?來追蹤引用所指向的值(也就是?解引用)。一旦解引用了?y
,就可以訪問?y
?所指向的整型值并可以與?5
?做比較。
像引用一樣使用Box<T>
可以使用?Box<T>
?代替引用來重寫示例中的代碼,解引用運算符也一樣能工作
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y); // 解引用
}
這個示例和上個示例唯一不同的地方就是將?y
?設(shè)置為一個指向?x
?值的 box 實例,而不是指向?x
?值的引用。在最后的斷言中,可以使用解引用運算符以?y
?為引用時相同的方式追蹤 box 的指針。接下來讓我們通過實現(xiàn)自己的 box 類型來探索?Box<T>
?能這么做有何特殊之處。
自定義智能指針
為了體會默認(rèn)情況下智能指針與引用的不同,讓我們創(chuàng)建一個類似于標(biāo)準(zhǔn)庫提供的?Box<T>
?類型的智能指針。接著學(xué)習(xí)如何增加使用解引用運算符的功能。
從根本上說,Box<T>
?被定義為包含一個元素的元組結(jié)構(gòu)體,所以下面示例以相同的方式定義了?MyBox<T>
?類型。我們還定義了?new
?函數(shù)來對應(yīng)定義于?Box<T>
?的?new
?函數(shù):
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
這里定義了一個結(jié)構(gòu)體?MyBox
?并聲明了一個泛型參數(shù)?T
,因為我們希望其可以存放任何類型的值。MyBox
?是一個包含?T
?類型元素的元組結(jié)構(gòu)體。MyBox::new
?函數(shù)獲取一個?T
?類型的參數(shù)并返回一個存放傳入值的?MyBox
?實例。替換成自己寫的:
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y); // 解引用
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
結(jié)果
MyBox<T>
?類型不能解引用,因為我們尚未在該類型實現(xiàn)這個功能。為了啟用?*
?運算符的解引用功能,需要實現(xiàn)?Deref
?trait。
通過實現(xiàn)Deref trait 將某類型像引用一樣處理
為了實現(xiàn) trait,需要提供 trait 所需的方法實現(xiàn)。
use std::ops::Deref;
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y); // 解引用
}
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
// 重點在這里
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
type Target = T;
?語法定義了用于此 trait 的關(guān)聯(lián)類型。關(guān)聯(lián)類型是一個稍有不同的定義泛型參數(shù)的方式,現(xiàn)在還無需過多的擔(dān)心它;第十九章會詳細(xì)介紹。
deref
?方法體中寫入了?&self.0
,這樣?deref
?返回了我希望通過?*
?運算符訪問的值的引用。最開始示例 中的?main
?函數(shù)中對?MyBox<T>
?值的?*
?調(diào)用現(xiàn)在可以編譯并能通過斷言了!
沒有?Deref
?trait 的話,編譯器只會解引用?&
?引用類型。deref
?方法向編譯器提供了獲取任何實現(xiàn)了?Deref
?trait 的類型的值,并且調(diào)用這個類型的?deref
?方法來獲取一個它知道如何解引用的?&
?引用的能力。
Rust 將?*
?運算符替換為先調(diào)用?deref
?方法再進(jìn)行普通解引用的操作,如此我們便不用擔(dān)心是否還需手動調(diào)用?deref
?方法了。Rust 的這個特性可以讓我們寫出行為一致的代碼,無論是面對的是常規(guī)引用還是實現(xiàn)了?Deref
?的類型。
deref
?方法返回值的引用,以及?*(y.deref())
?括號外邊的普通解引用仍為必須的原因在于所有權(quán)。如果?deref
?方法直接返回值而不是值的引用,其值(的所有權(quán))將被移出?self
。在這里以及大部分使用解引用運算符的情況下我們并不希望獲取?MyBox<T>
?內(nèi)部值的所有權(quán)。
函數(shù)和方法的隱式解引用強制多態(tài)
解引用強制多態(tài)(deref coercions)是 Rust 在函數(shù)或方法傳參上的一種便利。其將實現(xiàn)了?Deref
?的類型的引用轉(zhuǎn)換為原始類型通過?Deref
?所能夠轉(zhuǎn)換的類型的引用。當(dāng)這種特定類型的引用作為實參傳遞給和形參類型不同的函數(shù)或方法時,解引用強制多態(tài)將自動發(fā)生。這時會有一系列的?deref
?方法被調(diào)用,把我們提供的類型轉(zhuǎn)換成了參數(shù)所需的類型。
解引用強制多態(tài)的加入使得 Rust 程序員編寫函數(shù)和方法調(diào)用時無需增加過多顯式使用?&
?和?*
?的引用和解引用。這個功能也使得我們可以編寫更多同時作用于引用或智能指針的代碼。
作為展示解引用強制多態(tài)的實例,使用之前完整的代碼。再定義一個slice
fn hello(name: &str) {
println!("Hello, {}!", name);
}
可以使用字符串 slice 作為參數(shù)調(diào)用?hello
?函數(shù),比如?hello("Rust");
。解引用強制多態(tài)使得用?MyBox<String>
?類型值的引用調(diào)用?hello
?成為可能
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
這里使用?&m
?調(diào)用?hello
?函數(shù),其為?MyBox<String>
?值的引用。因為示例 15-10 中在?MyBox<T>
?上實現(xiàn)了?Deref
?trait,Rust 可以通過?deref
?調(diào)用將?&MyBox<String>
?變?yōu)?&String
。標(biāo)準(zhǔn)庫中提供了?String
?上的?Deref
?實現(xiàn),其會返回字符串 slice,這可以在?Deref
?的 API 文檔中看到。Rust 再次調(diào)用?deref
?將?&String
?變?yōu)?&str
,這就符合?hello
?函數(shù)的定義了。
如果 Rust 沒有實現(xiàn)解引用強制多態(tài),為了使用?&MyBox<String>
?類型的值調(diào)用?hello
,則不得不編寫示例
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
(*m)
?將?MyBox<String>
?解引用為?String
。接著?&
?和?[..]
?獲取了整個?String
?的字符串 slice 來匹配?hello
?的簽名。沒有解引用強制多態(tài)所有這些符號混在一起將更難以讀寫和理解。解引用強制多態(tài)使得 Rust 自動的幫我們處理這些轉(zhuǎn)換。
當(dāng)所涉及到的類型定義了?Deref
?trait,Rust 會分析這些類型并使用任意多次?Deref::deref
?調(diào)用以獲得匹配參數(shù)的類型。這些解析都發(fā)生在編譯時,所以利用解引用強制多態(tài)并沒有運行時懲罰!
解引用強制多態(tài)如何與可變性交互
類似于如何使用?Deref
?trait 重載不可變引用的?*
?運算符,Rust 提供了?DerefMut
?trait 用于重載可變引用的?*
?運算符。
Rust 在發(fā)現(xiàn)類型和 trait 實現(xiàn)滿足三種情況時會進(jìn)行解引用強制多態(tài):
- 當(dāng)?
T: Deref<Target=U>
?時從?&T
?到?&U
。 - 當(dāng)?
T: DerefMut<Target=U>
?時從?&mut T
?到?&mut U
。 - 當(dāng)?
T: Deref<Target=U>
?時從?&mut T
?到?&U
。
頭兩個情況除了可變性之外是相同的:第一種情況表明如果有一個?&T
,而?T
?實現(xiàn)了返回?U
?類型的?Deref
,則可以直接得到?&U
。第二種情況表明對于可變引用也有著相同的行為。
第三個情況有些微妙:Rust 也會將可變引用強轉(zhuǎn)為不可變引用。但是反之是?不可能?的:不可變引用永遠(yuǎn)也不能強轉(zhuǎn)為可變引用。因為根據(jù)借用規(guī)則,如果有一個可變引用,其必須是這些數(shù)據(jù)的唯一引用(否則程序?qū)o法編譯)。將一個可變引用轉(zhuǎn)換為不可變引用永遠(yuǎn)也不會打破借用規(guī)則。將不可變引用轉(zhuǎn)換為可變引用則需要數(shù)據(jù)只能有一個不可變引用,而借用規(guī)則無法保證這一點。因此,Rust 無法假設(shè)將不可變引用轉(zhuǎn)換為可變引用是可能的。
完整代碼
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
// 重點在這里
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
fn hello(name: &str) {
println!("Hello, {}!", name);
}
15.3?使用Drop Trait 運行清理代碼
對于智能指針模式來說第二個重要的 trait 是?Drop
,其允許我們在值要離開作用域時執(zhí)行一些代碼??梢詾槿魏晤愋吞峁?Drop
?trait 的實現(xiàn),同時所指定的代碼被用于釋放類似于文件或網(wǎng)絡(luò)連接的資源。我們在智能指針上下文中討論?Drop
?是因為其功能幾乎總是用于實現(xiàn)智能指針。例如,Box<T>
?自定義了?Drop
?用來釋放 box 所指向的堆空間。
在其他一些語言中,我們不得不記住在每次使用完智能指針實例后調(diào)用清理內(nèi)存或資源的代碼。如果忘記的話,運行代碼的系統(tǒng)可能會因為負(fù)荷過重而崩潰。在 Rust 中,可以指定每當(dāng)值離開作用域時被執(zhí)行的代碼,編譯器會自動插入這些代碼。于是我們就不需要在程序中到處編寫在實例結(jié)束時清理這些變量的代碼 —— 而且還不會泄露資源。
指定在值離開作用域時應(yīng)該執(zhí)行的代碼的方式是實現(xiàn)?Drop
?trait。Drop
?trait 要求實現(xiàn)一個叫做?drop
?的方法,它獲取一個?self
?的可變引用。為了能夠看出 Rust 何時調(diào)用?drop
,讓我們暫時使用?println!
?語句實現(xiàn)?drop
。
示例展示了唯一定制功能就是當(dāng)其實例離開作用域時,打印出?Dropping CustomSmartPointer!
?的結(jié)構(gòu)體?CustomSmartPointer
。這會演示 Rust 何時運行?drop
?函數(shù):
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer { data: String::from("my stuff") };
let d = CustomSmartPointer { data: String::from("other stuff") };
println!("CustomSmartPointers created.");
}
Drop
?trait 包含在 prelude 中,所以無需導(dǎo)入它。我們在?CustomSmartPointer
?上實現(xiàn)了?Drop
?trait,并提供了一個調(diào)用?println!
?的?drop
?方法實現(xiàn)。drop
?函數(shù)體是放置任何當(dāng)類型實例離開作用域時期望運行的邏輯的地方。這里選擇打印一些文本以展示 Rust 何時調(diào)用?drop
。
在?main
?中,我們新建了兩個?CustomSmartPointer
?實例并打印出了?CustomSmartPointer created.
。在?main
?的結(jié)尾,CustomSmartPointer
?的實例會離開作用域,而 Rust 會調(diào)用放置于?drop
?方法中的代碼,打印出最后的信息。注意無需顯示調(diào)用?drop
?方法:
當(dāng)實例離開作用域 Rust 會自動調(diào)用?drop
,并調(diào)用我們指定的代碼。變量以被創(chuàng)建時相反的順序被丟棄,所以?d
?在?c
?之前被丟棄。這個例子剛好給了我們一個 drop 方法如何工作的可視化指導(dǎo),不過通常需要指定類型所需執(zhí)行的清理代碼而不是打印信息。
通過std : :mem: : drop 提早丟棄值
不幸的是,我們并不能直截了當(dāng)?shù)慕?drop
?這個功能。通常也不需要禁用?drop
?;整個?Drop
?trait 存在的意義在于其是自動處理的。然而,有時你可能需要提早清理某個值。一個例子是當(dāng)使用智能指針管理鎖時;你可能希望強制運行?drop
?方法來釋放鎖以便作用域中的其他代碼可以獲取鎖。Rust 并不允許我們主動調(diào)用?Drop
?trait 的?drop
?方法;當(dāng)我們希望在作用域結(jié)束之前就強制釋放變量的話,我們應(yīng)該使用的是由標(biāo)準(zhǔn)庫提供的?std::mem::drop
。
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer { data: String::from("my stuff") };
c.drop();
println!("CustomSmartPointers created.");
}
結(jié)果
錯誤信息表明不允許顯式調(diào)用?drop
。錯誤信息使用了術(shù)語?析構(gòu)函數(shù)(destructor),這是一個清理實例的函數(shù)的通用編程概念。析構(gòu)函數(shù)?對應(yīng)創(chuàng)建實例的?構(gòu)造函數(shù)。Rust 中的?drop
?函數(shù)就是這么一個析構(gòu)函數(shù)。
Rust 不允許我們顯式調(diào)用?drop
?因為 Rust 仍然會在?main
?的結(jié)尾對值自動調(diào)用?drop
,這會導(dǎo)致一個?double free?錯誤,因為 Rust 會嘗試清理相同的值兩次。
因為不能禁用當(dāng)值離開作用域時自動插入的?drop
,并且不能顯式調(diào)用?drop
,如果我們需要強制提早清理值,可以使用?std::mem::drop
?函數(shù)。
std::mem::drop
?函數(shù)不同于?Drop
?trait 中的?drop
?方法??梢酝ㄟ^傳遞希望提早強制丟棄的值作為參數(shù)。std::mem::drop
?位于 prelude。
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer { data: String::from("some data") };
println!("CustomSmartPointer created.");
drop(c);
println!("CustomSmartPointer dropped before the end of main.");
}
運行這段代碼會打印出如下:
?Dropping CustomSmartPointer with data `some data`!
?出現(xiàn)在?CustomSmartPointer created.
?和?CustomSmartPointer dropped before the end of main.
?之間,表明了?drop
?方法被調(diào)用了并在此丟棄了?c
。
Drop
?trait 實現(xiàn)中指定的代碼可以用于許多方面,來使得清理變得方便和安全:比如可以用其創(chuàng)建我們自己的內(nèi)存分配器!通過?Drop
?trait 和 Rust 所有權(quán)系統(tǒng),你無需擔(dān)心之后的代碼清理,Rust 會自動考慮這些問題。
我們也無需擔(dān)心意外的清理掉仍在使用的值,這會造成編譯器錯誤:所有權(quán)系統(tǒng)確保引用總是有效的,也會確保?drop
?只會在值不再被使用時被調(diào)用一次。
15.4?Rc<T>引用計數(shù)智能指針
大部分情況下所有權(quán)是非常明確的:可以準(zhǔn)確地知道哪個變量擁有某個值。然而,有些情況單個值可能會有多個所有者。例如,在圖數(shù)據(jù)結(jié)構(gòu)中,多個邊可能指向相同的結(jié)點,而這個結(jié)點從概念上講為所有指向它的邊所擁有。結(jié)點直到?jīng)]有任何邊指向它之前都不應(yīng)該被清理。
為了啟用多所有權(quán),Rust 有一個叫做?Rc<T>
?的類型。其名稱為?引用計數(shù)(reference counting)的縮寫。引用計數(shù)意味著記錄一個值引用的數(shù)量來知曉這個值是否仍在被使用。如果某個值有零個引用,就代表沒有任何有效引用并可以被清理。
可以將其想象為客廳中的電視。當(dāng)一個人進(jìn)來看電視時,他打開電視。其他人也可以進(jìn)來看電視。當(dāng)最后一個人離開房間時,他關(guān)掉電視因為它不再被使用了。如果某人在其他人還在看的時候就關(guān)掉了電視,正在看電視的人肯定會抓狂的!
Rc<T>
?用于當(dāng)我們希望在堆上分配一些內(nèi)存供程序的多個部分讀取,而且無法在編譯時確定程序的哪一部分會最后結(jié)束使用它的時候。如果確實知道哪部分是最后一個結(jié)束使用的話,就可以令其成為數(shù)據(jù)的所有者,正常的所有權(quán)規(guī)則就可以在編譯時生效。
注意?Rc<T>
?只能用于單線程場景;第十六章并發(fā)會涉及到如何在多線程程序中進(jìn)行引用計數(shù)。
使用Rc<T>共享數(shù)據(jù)
讓我們回到前面示例中使用?Box<T>
?定義 cons list 的例子。這一次,我們希望創(chuàng)建兩個共享第三個列表所有權(quán)的列表,其概念將會看起來如圖所示:
列表?a
?包含 5 之后是 10,之后是另兩個列表:b
?從 3 開始而?c
?從 4 開始。b
?和?c
?會接上包含 5 和 10 的列表?a
。換句話說,這兩個列表會嘗試共享第一個列表所包含的 5 和 10。
嘗試使用?Box<T>
?定義的?List
?并實現(xiàn)不能工作
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));
}
結(jié)果
Cons
?成員擁有其儲存的數(shù)據(jù),所以當(dāng)創(chuàng)建?b
?列表時,a
?被移動進(jìn)了?b
?這樣?b
?就擁有了?a
。接著當(dāng)再次嘗使用?a
?創(chuàng)建?c
?時,這不被允許因為?a
?的所有權(quán)已經(jīng)被移動。
可以改變?Cons
?的定義來存放一個引用,不過接著必須指定生命周期參數(shù)。通過指定生命周期參數(shù),表明列表中的每一個元素都至少與列表本身存在的一樣久。例如,借用檢查器不會允許?let a = Cons(10, &Nil);
?編譯,因為臨時值?Nil
?會在?a
?獲取其引用之前就被丟棄了。
相反,我們修改?List
?的定義為使用?Rc<T>
?代替?Box<T>
,如下例所示?,F(xiàn)在每一個?Cons
?變量都包含一個值和一個指向?List
?的?Rc
。當(dāng)創(chuàng)建?b
?時,不同于獲取?a
?的所有權(quán),這里會克隆?a
?所包含的?Rc
,這會將引用計數(shù)從 1 增加到 2 并允許?a
?和?b
?共享?Rc
?中數(shù)據(jù)的所有權(quán)。創(chuàng)建?c
?時也會克隆?a
,這會將引用計數(shù)從 2 增加為 3。每次調(diào)用?Rc::clone
,Rc
?中數(shù)據(jù)的引用計數(shù)都會增加,直到有零個引用之前其數(shù)據(jù)都不會被清理。
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
?中創(chuàng)建了存放 5 和 10 的列表并將其存放在?a
?的新的?Rc<List>
?中。接著當(dāng)創(chuàng)建?b
?和?c
?時,調(diào)用?Rc::clone
?函數(shù)并傳遞?a
?中?Rc<List>
?的引用作為參數(shù)。
也可以調(diào)用?a.clone()
?而不是?Rc::clone(&a)
,不過在這里 Rust 的習(xí)慣是使用?Rc::clone
。Rc::clone
?的實現(xiàn)并不像大部分類型的?clone
?實現(xiàn)那樣對所有數(shù)據(jù)進(jìn)行深拷貝。Rc::clone
?只會增加引用計數(shù),這并不會花費多少時間。深拷貝可能會花費很長時間。通過使用?Rc::clone
?進(jìn)行引用計數(shù),可以明顯的區(qū)別深拷貝類的克隆和增加引用計數(shù)類的克隆。當(dāng)查找代碼中的性能問題時,只需考慮深拷貝類的克隆而無需考慮?Rc::clone
?調(diào)用。
克隆Rc<T>會增加引用計數(shù)
讓我們修改示例的代碼以便觀察創(chuàng)建和丟棄?a
?中?Rc<List>
?的引用時引用計數(shù)的變化。
在下面示例中,修改了?main
?以便將列表?c
?置于內(nèi)部作用域中,這樣就可以觀察當(dāng)?c
?離開作用域時引用計數(shù)如何變化。
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));
}
結(jié)果
在程序中每個引用計數(shù)變化的點,會打印出引用計數(shù),其值可以通過調(diào)用?Rc::strong_count
?函數(shù)獲得。這個函數(shù)叫做?strong_count
?而不是?count
?是因為?Rc<T>
?也有?weak_count
;在?“避免引用循環(huán):將?Rc<T>?變?yōu)?Weak<T>”?部分會講解?weak_count
?的用途。
我們能夠看到?a
?中?Rc<List>
?的初始引用計數(shù)為1,接著每次調(diào)用?clone
,計數(shù)會增加1。當(dāng)?c
?離開作用域時,計數(shù)減1。不必像調(diào)用?Rc::clone
?增加引用計數(shù)那樣調(diào)用一個函數(shù)來減少計數(shù);Drop
?trait 的實現(xiàn)當(dāng)?Rc<T>
?值離開作用域時自動減少引用計數(shù)。
從這個例子我們所不能看到的是,在?main
?的結(jié)尾當(dāng)?b
?然后是?a
?離開作用域時,此處計數(shù)會是 0,同時?Rc
?被完全清理。使用?Rc
?允許一個值有多個所有者,引用計數(shù)則確保只要任何所有者依然存在其值也保持有效。
通過不可變引用,?Rc<T>
?允許在程序的多個部分之間只讀地共享數(shù)據(jù)。如果?Rc<T>
?也允許多個可變引用,則會違反第四章討論的借用規(guī)則之一:相同位置的多個可變借用可能造成數(shù)據(jù)競爭和不一致。不過可以修改數(shù)據(jù)是非常有用的!在下一部分,我們將討論內(nèi)部可變性模式和?RefCell<T>
?類型,它可以與?Rc<T>
?結(jié)合使用來處理不可變性的限制。
15.5?RefCe11<T>和內(nèi)部可變性模式
內(nèi)部可變性(Interior mutability)是 Rust 中的一個設(shè)計模式,它允許你即使在有不可變引用時也可以改變數(shù)據(jù),這通常是借用規(guī)則所不允許的。為了改變數(shù)據(jù),該模式在數(shù)據(jù)結(jié)構(gòu)中使用?unsafe
?代碼來模糊 Rust 通常的可變性和借用規(guī)則。我們還未講到不安全代碼;第十九章會學(xué)習(xí)它們。當(dāng)可以確保代碼在運行時會遵守借用規(guī)則,即使編譯器不能保證的情況,可以選擇使用那些運用內(nèi)部可變性模式的類型。所涉及的?unsafe
?代碼將被封裝進(jìn)安全的 API 中,而外部類型仍然是不可變的。
通過RefCell<T>在運行時檢查借用規(guī)則
不同于?Rc<T>
,RefCell<T>
?代表其數(shù)據(jù)的唯一的所有權(quán)。那么是什么讓?RefCell<T>
?不同于像?Box<T>
?這樣的類型呢?回憶一下第四章所學(xué)的借用規(guī)則:
- 在任意給定時間,只能擁有一個可變引用或任意數(shù)量的不可變引用?之一(而不是全部)。
- 引用必須總是有效的。
對于引用和?Box<T>
,借用規(guī)則的不可變性作用于編譯時。對于?RefCell<T>
,這些不可變性作用于?運行時。對于引用,如果違反這些規(guī)則,會得到一個編譯錯誤。而對于?RefCell<T>
,如果違反這些規(guī)則程序會 panic 并退出。
在編譯時檢查借用規(guī)則的優(yōu)勢是這些錯誤將在開發(fā)過程的早期被捕獲,同時對運行時沒有性能影響,因為所有的分析都提前完成了。為此,在編譯時檢查借用規(guī)則是大部分情況的最佳選擇,這也正是其為何是 Rust 的默認(rèn)行為。
相反在運行時檢查借用規(guī)則的好處則是允許出現(xiàn)特定內(nèi)存安全的場景,而它們在編譯時檢查中是不允許的。靜態(tài)分析,正如 Rust 編譯器,是天生保守的。但代碼的一些屬性不可能通過分析代碼發(fā)現(xiàn):其中最著名的就是?停機問題(Halting Problem),這超出了本書的范疇,不過如果你感興趣的話這是一個值得研究的有趣主題。
因為一些分析是不可能的,如果 Rust 編譯器不能通過所有權(quán)規(guī)則編譯,它可能會拒絕一個正確的程序;從這種角度考慮它是保守的。如果 Rust 接受不正確的程序,那么用戶也就不會相信 Rust 所做的保證了。然而,如果 Rust 拒絕正確的程序,雖然會給程序員帶來不便,但不會帶來災(zāi)難。RefCell<T>
?正是用于當(dāng)你確信代碼遵守借用規(guī)則,而編譯器不能理解和確定的時候。
類似于?Rc<T>
,RefCell<T>
?只能用于單線程場景。如果嘗試在多線程上下文中使用RefCell<T>
,會得到一個編譯錯誤。第十六章會介紹如何在多線程程序中使用?RefCell<T>
?的功能。
如下為選擇?Box<T>
,Rc<T>
?或?RefCell<T>
?的理由:
-
Rc<T>
?允許相同數(shù)據(jù)有多個所有者;Box<T>
?和?RefCell<T>
?有單一所有者。 -
Box<T>
?允許在編譯時執(zhí)行不可變或可變借用檢查;Rc<T>
僅允許在編譯時執(zhí)行不可變借用檢查;RefCell<T>
?允許在運行時執(zhí)行不可變或可變借用檢查。 - 因為?
RefCell<T>
?允許在運行時執(zhí)行可變借用檢查,所以我們可以在即便?RefCell<T>
?自身是不可變的情況下修改其內(nèi)部的值。
在不可變值內(nèi)部改變值就是?內(nèi)部可變性?模式。讓我們看看何時內(nèi)部可變性是有用的,并討論這是如何成為可能的。
內(nèi)部可變性:不可變值的可變借用
借用規(guī)則的一個推論是當(dāng)有一個不可變值時,不能可變地借用它。例如,如下代碼不能編譯:
fn main() {
let x = 5;
let y = &mut x;
}
結(jié)果
然而,特定情況下在值的方法內(nèi)部能夠修改自身是很有用的,而不是在其他代碼中。此時值仍然是不可變的,值方法外部的代碼不能修改其值。RefCell<T>
?是一個獲得內(nèi)部可變性的方法。RefCell<T>
?并沒有完全繞開借用規(guī)則,編譯器中的借用檢查器允許內(nèi)部可變性并相應(yīng)地在運行時檢查借用規(guī)則。如果違反了這些規(guī)則,會得到?panic!
?而不是編譯錯誤。
內(nèi)部可變性的用例: mock對象
測試替身(test double)是一個通用編程概念,它代表一個在測試中替代某個類型的類型。mock 對象?是特定類型的測試替身,它們記錄測試過程中發(fā)生了什么以便可以斷言操作是正確的。
雖然 Rust 沒有與其他語言中的對象完全相同的對象,Rust 也沒有像其他語言那樣在標(biāo)準(zhǔn)庫中內(nèi)建 mock 對象功能,不過我們確實可以創(chuàng)建一個與 mock 對象有著相同功能的結(jié)構(gòu)體。
如下是一個我們想要測試的場景:我們在編寫一個記錄某個值與最大值的差距的庫,并根據(jù)當(dāng)前值與最大值的差距來發(fā)送消息。例如,這個庫可以用于記錄用戶所允許的 API 調(diào)用數(shù)量限額。
該庫只提供記錄與最大值的差距,以及何種情況發(fā)送什么消息的功能。使用此庫的程序則期望提供實際發(fā)送消息的機制:程序可以選擇記錄一條消息、發(fā)送 email、發(fā)送短信等等。庫本身無需知道這些細(xì)節(jié);只需實現(xiàn)其提供的?Messenger
?trait 即可。文件名: src/lib.rs
fn main() {
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where T: Messenger {
pub fn new(messenger: &T, max: usize) -> LimitTracker<T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger.send("Warning: You've used up over 75% of your quota!");
}
}
}
}
這些代碼中一個重要部分是擁有一個方法?send
?的?Messenger
?trait,其獲取一個?self
?的不可變引用和文本信息。這是我們的 mock 對象所需要擁有的接口。另一個重要的部分是我們需要測試?LimitTracker
?的?set_value
?方法的行為??梢愿淖儌鬟f的?value
?參數(shù)的值,不過?set_value
?并沒有返回任何可供斷言的值。也就是說,如果使用某個實現(xiàn)了?Messenger
?trait 的值和特定的?max
?創(chuàng)建?LimitTracker
,當(dāng)傳遞不同?value
?值時,消息發(fā)送者應(yīng)被告知發(fā)送合適的消息。
我們所需的 mock 對象是,調(diào)用?send
?不同于實際發(fā)送 email 或短息,其只記錄信息被通知要發(fā)送了??梢孕陆ㄒ粋€ mock 對象示例,用其創(chuàng)建?LimitTracker
,調(diào)用?LimitTracker
?的?set_value
?方法,然后檢查 mock 對象是否有我們期望的消息。文件名: src/lib.rs
#[cfg(test)]
mod tests {
use super::*;
struct MockMessenger {
sent_messages: Vec<String>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger { sent_messages: vec![] }
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.len(), 1);
}
}
測試代碼定義了一個?MockMessenger
?結(jié)構(gòu)體,其?sent_messages
?字段為一個?String
?值的?Vec
?用來記錄被告知發(fā)送的消息。我們還定義了一個關(guān)聯(lián)函數(shù)?new
?以便于新建從空消息列表開始的?MockMessenger
?值。接著為?MockMessenger
?實現(xiàn)?Messenger
?trait 這樣就可以為?LimitTracker
?提供一個?MockMessenger
。在?send
?方法的定義中,獲取傳入的消息作為參數(shù)并儲存在?MockMessenger
?的?sent_messages
?列表中。
在測試中,我們測試了當(dāng)?LimitTracker
?被告知將?value
?設(shè)置為超過?max
?值 75% 的某個值。首先新建一個?MockMessenger
,其從空消息列表開始。接著新建一個?LimitTracker
?并傳遞新建?MockMessenger
?的引用和?max
?值 100。我們使用值 80 調(diào)用?LimitTracker
?的?set_value
?方法,這超過了 100 的 75%。接著斷言?MockMessenger
?中記錄的消息列表應(yīng)該有一條消息。
不能修改?MockMessenger
?來記錄消息,因為?send
?方法獲取了?self
?的不可變引用。我們也不能參考錯誤文本的建議使用?&mut self
?替代,因為這樣?send
?的簽名就不符合?Messenger
?trait 定義中的簽名了(可以試著這么改,看看會出現(xiàn)什么錯誤信息)。
這正是內(nèi)部可變性的用武之地!我們將通過?RefCell
?來儲存?sent_messages
,然后?send
?將能夠修改?sent_messages
?并儲存消息。文件名: src/lib.rs
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where T: Messenger {
pub fn new(messenger: &T, max: usize) -> LimitTracker<T> {
LimitTracker {
messenger,
value: 0,
max,
}
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger.send("Urgent warning: You've used up over 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger { sent_messages: RefCell::new(vec![]) }
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
// --snip--
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(75);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
fn main() {}
現(xiàn)在?sent_messages
?字段的類型是?RefCell<Vec<String>>
?而不是?Vec<String>
。在?new
?函數(shù)中新建了一個?RefCell
?示例替代空 vector。
對于?send
?方法的實現(xiàn),第一個參數(shù)仍為?self
?的不可變借用,這是符合方法定義的。我們調(diào)用?self.sent_messages
?中?RefCell
?的?borrow_mut
?方法來獲取?RefCell
?中值的可變引用,這是一個 vector。接著可以對 vector 的可變引用調(diào)用?push
?以便記錄測試過程中看到的消息。
最后必須做出的修改位于斷言中:為了看到其內(nèi)部 vector 中有多少個項,需要調(diào)用?RefCell
?的?borrow
?以獲取 vector 的不可變引用。
RefCe11<T>在運行時記錄借用
當(dāng)創(chuàng)建不可變和可變引用時,我們分別使用?&
?和?&mut
?語法。對于?RefCell<T>
?來說,則是?borrow
?和?borrow_mut
?方法,這屬于?RefCell<T>
?安全 API 的一部分。borrow
?方法返回?Ref
?類型的智能指針,borrow_mut
?方法返回?RefMut
?類型的智能指針。這兩個類型都實現(xiàn)了?Deref
,所以可以當(dāng)作常規(guī)引用對待。
RefCell<T>
?記錄當(dāng)前有多少個活動的?Ref<T>
?和?RefMut<T>
?智能指針。每次調(diào)用?borrow
,RefCell<T>
?將活動的不可變借用計數(shù)加一。當(dāng)?Ref
?值離開作用域時,不可變借用計數(shù)減一。就像編譯時借用規(guī)則一樣,RefCell<T>
?在任何時候只允許有多個不可變借用或一個可變借用。
如果我們嘗試違反這些規(guī)則,相比引用時的編譯時錯誤,RefCell<T>
?的實現(xiàn)會在運行時?panic!
。
結(jié)合Rc<T>和RefCell<T>來擁有多個可變數(shù)據(jù)所有者
RefCell<T>
?的一個常見用法是與?Rc<T>
?結(jié)合?;貞浺幌?Rc<T>
?允許對相同數(shù)據(jù)有多個所有者,不過只能提供數(shù)據(jù)的不可變訪問。如果有一個儲存了?RefCell<T>
?的?Rc<T>
?的話,就可以得到有多個所有者?并且?可以修改的值了!
例如,回憶示例 cons list 的例子中使用?Rc<T>
?使得多個列表共享另一個列表的所有權(quán)。因為?Rc<T>
?只存放不可變值,所以一旦創(chuàng)建了這些列表值后就不能修改。讓我們加入?RefCell<T>
?來獲得修改列表中值的能力。
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
use std::cell::RefCell;
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(6)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));
*value.borrow_mut() += 10;
println!("a after = {:?}", a);
println!("b after = {:?}", b);
println!("c after = {:?}", c);
}
這里創(chuàng)建了一個?Rc<RefCell<i32>>
?實例并儲存在變量?value
?中以便之后直接訪問。接著在?a
?中用包含?value
?的?Cons
?成員創(chuàng)建了一個?List
。需要克隆?value
?以便?a
?和?value
?都能擁有其內(nèi)部值?5
?的所有權(quán),而不是將所有權(quán)從?value
?移動到?a
?或者讓?a
?借用?value
。
我們將列表?a
?封裝進(jìn)了?Rc<T>
?這樣當(dāng)創(chuàng)建列表?b
?和?c
?時,他們都可以引用?a
,正如示例 15-18 一樣。
一旦創(chuàng)建了列表?a
、b
?和?c
,我們將?value
?的值加 10。為此對?value
?調(diào)用了?borrow_mut
,這里使用了第五章討論的自動解引用功能(“->?運算符到哪去了?”?部分)來解引用?Rc<T>
?以獲取其內(nèi)部的?RefCell<T>
?值。borrow_mut
?方法返回?RefMut<T>
?智能指針,可以對其使用解引用運算符并修改其內(nèi)部值。
當(dāng)我們打印出?a
、b
?和?c
?時,可以看到他們都擁有修改后的值 15 而不是 5:
這是非常巧妙的!通過使用?RefCell<T>
,我們可以擁有一個表面上不可變的?List
,不過可以使用?RefCell<T>
?中提供內(nèi)部可變性的方法來在需要時修改數(shù)據(jù)。RefCell<T>
?的運行時借用規(guī)則檢查也確實保護我們免于出現(xiàn)數(shù)據(jù)競爭——有時為了數(shù)據(jù)結(jié)構(gòu)的靈活性而付出一些性能是值得的。
標(biāo)準(zhǔn)庫中也有其他提供內(nèi)部可變性的類型,比如?Cell<T>
,它有些類似?RefCell<T>
,除了提供內(nèi)部值的引用,其值還會被拷貝進(jìn)和拷貝出?Cell<T>
。
15.6?引用循環(huán)與內(nèi)存泄漏
Rust 的內(nèi)存安全性保證使其難以意外地制造永遠(yuǎn)也不會被清理的內(nèi)存(被稱為?內(nèi)存泄露(memory leak)),但并不是不可能。與在編譯時拒絕數(shù)據(jù)競爭不同, Rust 并不保證完全地避免內(nèi)存泄露,這意味著內(nèi)存泄露在 Rust 被認(rèn)為是內(nèi)存安全的。這一點可以通過?Rc<T>
?和?RefCell<T>
?看出:創(chuàng)建引用循環(huán)的可能性是存在的。這會造成內(nèi)存泄露,因為每一項的引用計數(shù)永遠(yuǎn)也到不了 0,其值也永遠(yuǎn)不會被丟棄。
制造引用循環(huán)
讓我們看看引用循環(huán)是如何發(fā)生的以及如何避免它。
fn main() {}
use std::rc::Rc;
use std::cell::RefCell;
use crate::List::{Cons, Nil};
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
}
impl List {
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self {
Cons(_, item) => Some(item),
Nil => None,
}
}
}
這里采用了示例 中?List
?定義的另一種變體?,F(xiàn)在?Cons
?成員的第二個元素是?RefCell<Rc<List>>
,這意味著不同于像示例 那樣能夠修改?i32
?的值,我們希望能夠修改?Cons
?成員所指向的?List
。這里還增加了一個?tail
?方法來方便我們在有?Cons
?成員的時候訪問其第二項。
在示例中增加了一個?main
?函數(shù),其使用了示例中的定義。這些代碼在?a
?中創(chuàng)建了一個列表,一個指向?a
?中列表的?b
?列表,接著修改?b
?中的列表指向?a
?中的列表,這會創(chuàng)建一個引用循環(huán)。在這個過程的多個位置有?println!
?語句展示引用計數(shù)。
use crate::List::{Cons, Nil};
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil,
}
impl List {
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self {
Cons(_, item) => Some(item),
Nil => None,
}
}
}
fn main() {
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
println!("a initial rc count = {}", Rc::strong_count(&a));
println!("a next item = {:?}", a.tail());
let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
println!("a rc count after b creation = {}", Rc::strong_count(&a));
println!("b initial rc count = {}", Rc::strong_count(&b));
println!("b next item = {:?}", b.tail());
if let Some(link) = a.tail() {
*link.borrow_mut() = Rc::clone(&b);
}
println!("b rc count after changing a = {}", Rc::strong_count(&b));
println!("a rc count after changing a = {}", Rc::strong_count(&a));
// Uncomment the next line to see that we have a cycle;
// it will overflow the stack
// println!("a next item = {:?}", a.tail());
}
這里在變量?a
?中創(chuàng)建了一個?Rc<List>
?實例來存放初值為?5, Nil
?的?List
?值。接著在變量?b
?中創(chuàng)建了存放包含值 10 和指向列表?a
?的?List
?的另一個?Rc<List>
?實例。
最后,修改?a
?使其指向?b
?而不是?Nil
,這就創(chuàng)建了一個循環(huán)。為此需要使用?tail
?方法獲取?a
?中?RefCell<Rc<List>>
?的引用,并放入變量?link
?中。接著使用?RefCell<Rc<List>>
?的?borrow_mut
?方法將其值從存放?Nil
?的?Rc
?修改為?b
?中的?Rc<List>
。
可以看到將?a
?修改為指向?b
?之后,a
?和?b
?中都有的?Rc<List>
?實例的引用計數(shù)為 2。在?main
?的結(jié)尾,Rust 會嘗試首先丟棄?b
,這會使?a
?和?b
?中?Rc
?實例的引用計數(shù)減 1。
然而,因為?a
?仍然引用?b
?中的?Rc<List>
,Rc<List>
?的引用計數(shù)是 1 而不是 0,所以?Rc<List>
?在堆上的內(nèi)存不會被丟棄。其內(nèi)存會因為引用計數(shù)為 1 而永遠(yuǎn)停留。為了更形象的展示,我們創(chuàng)建了一個如圖所示的引用循環(huán):
如果取消最后?println!
?的注釋并運行程序,Rust 會嘗試打印出?a
?指向?b
?指向?a
?這樣的循環(huán)直到棧溢出。
這個特定的例子中,創(chuàng)建了引用循環(huán)之后程序立刻就結(jié)束了。這個循環(huán)的結(jié)果并不可怕。如果在更為復(fù)雜的程序中并在循環(huán)里分配了很多內(nèi)存并占有很長時間,這個程序會使用多于它所需要的內(nèi)存,并有可能壓垮系統(tǒng)并造成沒有內(nèi)存可供使用。
創(chuàng)建引用循環(huán)并不容易,但也不是不可能。如果你有包含?Rc<T>
?的?RefCell<T>
?值或類似的嵌套結(jié)合了內(nèi)部可變性和引用計數(shù)的類型,請務(wù)必小心確保你沒有形成一個引用循環(huán);你無法指望 Rust 幫你捕獲它們。創(chuàng)建引用循環(huán)是一個程序上的邏輯 bug,你應(yīng)該使用自動化測試、代碼評審和其他軟件開發(fā)最佳實踐來使其最小化。
另一個解決方案是重新組織數(shù)據(jù)結(jié)構(gòu),使得一部分引用擁有所有權(quán)而另一部分沒有。換句話說,循環(huán)將由一些擁有所有權(quán)的關(guān)系和一些無所有權(quán)的關(guān)系組成,只有所有權(quán)關(guān)系才能影響值是否可以被丟棄。
避免引用循環(huán):將Rc<T>變?yōu)閣eak<T>
到目前為止,我們已經(jīng)展示了調(diào)用?Rc::clone
?會增加?Rc<T>
?實例的?strong_count
,和只在其?strong_count
?為 0 時才會被清理的?Rc<T>
?實例。你也可以通過調(diào)用?Rc::downgrade
?并傳遞?Rc
?實例的引用來創(chuàng)建其值的?弱引用(weak reference)。調(diào)用?Rc::downgrade
?時會得到?Weak<T>
?類型的智能指針。不同于將?Rc<T>
?實例的?strong_count
?加1,調(diào)用?Rc::downgrade
?會將?weak_count
?加1。Rc<T>
?類型使用?weak_count
?來記錄其存在多少個?Weak<T>
?引用,類似于?strong_count
。其區(qū)別在于?weak_count
?無需計數(shù)為 0 就能使?Rc
?實例被清理。
強引用代表如何共享?Rc<T>
?實例的所有權(quán),但弱引用并不屬于所有權(quán)關(guān)系。他們不會造成引用循環(huán),因為任何弱引用的循環(huán)會在其相關(guān)的強引用計數(shù)為 0 時被打斷。
因為?Weak<T>
?引用的值可能已經(jīng)被丟棄了,為了使用?Weak<T>
?所指向的值,我們必須確保其值仍然有效。為此可以調(diào)用?Weak<T>
?實例的?upgrade
?方法,這會返回?Option<Rc<T>>
。如果?Rc<T>
?值還未被丟棄,則結(jié)果是?Some
;如果?Rc<T>
?已被丟棄,則結(jié)果是?None
。因為?upgrade
?返回一個?Option<T>
,我們確信 Rust 會處理?Some
?和?None
?的情況,所以它不會返回非法指針。
我們會創(chuàng)建一個某項知道其子項和父項的樹形結(jié)構(gòu)的例子,而不是只知道其下一項的列表。
創(chuàng)建樹形數(shù)據(jù)結(jié)構(gòu):帶有子結(jié)點的Node
在最開始,我們將會構(gòu)建一個帶有子節(jié)點的樹。讓我們創(chuàng)建一個用于存放其擁有所有權(quán)的?i32
?值和其子節(jié)點引用的?Node
:
fn main() {
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}
}
我們希望能夠?Node
?擁有其子結(jié)點,同時也希望通過變量來共享所有權(quán),以便可以直接訪問樹中的每一個?Node
,為此?Vec<T>
?的項的類型被定義為?Rc<Node>
。我們還希望能修改其他結(jié)點的子結(jié)點,所以?children
?中?Vec<Rc<Node>>
?被放進(jìn)了?RefCell<T>
。
接下來,使用此結(jié)構(gòu)體定義來創(chuàng)建一個叫做?leaf
?的帶有值 3 且沒有子結(jié)點的?Node
?實例,和另一個帶有值 5 并以?leaf
?作為子結(jié)點的實例?branch
use std::rc::Rc;
use std::cell::RefCell;
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
children: RefCell::new(vec![]),
});
let branch = Rc::new(Node {
value: 5,
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
}
這里克隆了?leaf
?中的?Rc<Node>
?并儲存在了?branch
?中,這意味著?leaf
?中的?Node
?現(xiàn)在有兩個所有者:leaf
和branch
??梢酝ㄟ^?branch.children
?從?branch
?中獲得?leaf
,不過無法從?leaf
?到?branch
。leaf
?沒有到?branch
?的引用且并不知道他們相互關(guān)聯(lián)。我們希望?leaf
?知道?branch
?是其父結(jié)點。稍后我們會這么做。
增加從子到父的引用
為了使子結(jié)點知道其父結(jié)點,需要在?Node
?結(jié)構(gòu)體定義中增加一個?parent
?字段。問題是?parent
?的類型應(yīng)該是什么。我們知道其不能包含?Rc<T>
,因為這樣?leaf.parent
?將會指向?branch
?而?branch.children
?會包含?leaf
?的指針,這會形成引用循環(huán),會造成其?strong_count
?永遠(yuǎn)也不會為 0.
現(xiàn)在換一種方式思考這個關(guān)系,父結(jié)點應(yīng)該擁有其子結(jié)點:如果父結(jié)點被丟棄了,其子結(jié)點也應(yīng)該被丟棄。然而子結(jié)點不應(yīng)該擁有其父結(jié)點:如果丟棄子結(jié)點,其父結(jié)點應(yīng)該依然存在。這正是弱引用的例子!
所以?parent
?使用?Weak<T>
?類型而不是?Rc<T>
,具體來說是?RefCell<Weak<Node>>
。現(xiàn)在?Node
?結(jié)構(gòu)體定義看起來像這樣:
fn main() {
use std::rc::{Rc, Weak};
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
}
這樣,一個結(jié)點就能夠引用其父結(jié)點,但不擁有其父結(jié)點。
use std::rc::{Rc, Weak};
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}
當(dāng)創(chuàng)建?branch
?結(jié)點時,其也會新建一個?Weak<Node>
?引用,因為?branch
?并沒有父結(jié)點。leaf
?仍然作為?branch
?的一個子結(jié)點。一旦在?branch
?中有了?Node
?實例,就可以修改?leaf
?使其擁有指向父結(jié)點的?Weak<Node>
?引用。這里使用了?leaf
?中?parent
?字段里的?RefCell<Weak<Node>>
?的?borrow_mut
?方法,接著使用了?Rc::downgrade
?函數(shù)來從?branch
?中的?Rc
?值創(chuàng)建了一個指向?branch
?的?Weak<Node>
?引用。
可視化strong_count和weak_count的改變
讓我們通過創(chuàng)建了一個新的內(nèi)部作用域并將?branch
?的創(chuàng)建放入其中,來觀察?Rc<Node>
?實例的?strong_count
?和?weak_count
?值的變化。這會展示當(dāng)?branch
?創(chuàng)建和離開作用域被丟棄時會發(fā)生什么。
use std::rc::{Rc, Weak};
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
{
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!(
"branch strong = {}, weak = {}",
Rc::strong_count(&branch),
Rc::weak_count(&branch),
);
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
}
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
println!(
"leaf strong = {}, weak = {}",
Rc::strong_count(&leaf),
Rc::weak_count(&leaf),
);
}
一旦創(chuàng)建了?leaf
,其?Rc<Node>
?的強引用計數(shù)為 1,弱引用計數(shù)為 0。在內(nèi)部作用域中創(chuàng)建了?branch
?并與?leaf
?相關(guān)聯(lián),此時?branch
?中?Rc<Node>
?的強引用計數(shù)為 1,弱引用計數(shù)為 1(因為?leaf.parent
?通過?Weak<Node>
?指向?branch
)。這里?leaf
?的強引用計數(shù)為 2,因為現(xiàn)在?branch
?的?branch.children
?中儲存了?leaf
?的?Rc<Node>
?的拷貝,不過弱引用計數(shù)仍然為 0。
當(dāng)內(nèi)部作用域結(jié)束時,branch
?離開作用域,Rc<Node>
?的強引用計數(shù)減少為 0,所以其?Node
?被丟棄。來自?leaf.parent
?的弱引用計數(shù) 1 與?Node
?是否被丟棄無關(guān),所以并沒有產(chǎn)生任何內(nèi)存泄露!
如果在內(nèi)部作用域結(jié)束后嘗試訪問?leaf
?的父結(jié)點,會再次得到?None
。在程序的結(jié)尾,leaf
?中?Rc<Node>
?的強引用計數(shù)為 1,弱引用計數(shù)為 0,因為現(xiàn)在?leaf
?又是?Rc<Node>
?唯一的引用了。
所有這些管理計數(shù)和值的邏輯都內(nèi)建于?Rc<T>
?和?Weak<T>
?以及它們的?Drop
?trait 實現(xiàn)中。通過在?Node
?定義中指定從子結(jié)點到父結(jié)點的關(guān)系為一個Weak<T>
引用,就能夠擁有父結(jié)點和子結(jié)點之間的雙向引用而不會造成引用循環(huán)和內(nèi)存泄露。文章來源:http://www.zghlxwxcb.cn/news/detail-655733.html
參考:智能指針 - Rust 程序設(shè)計語言 簡體中文版 (bootcss.com)文章來源地址http://www.zghlxwxcb.cn/news/detail-655733.html
到了這里,關(guān)于【Rust】Rust學(xué)習(xí) 第十五章智能指針的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!