鍍金池/ 教程/ Java/ 選擇你的保證
哲學(xué)家就餐問(wèn)題
鏈接進(jìn)階
名詞中英文對(duì)照
測(cè)試
引用和借用
泛型
方法語(yǔ)法
函數(shù)
不安全代碼
并發(fā)
裝箱語(yǔ)法和模式
注釋
棧和堆
運(yùn)算符與重載
語(yǔ)法索引
文檔
固有功能
所有權(quán)
循環(huán)
通用函數(shù)調(diào)用語(yǔ)法
不定長(zhǎng)類型
<code>const</code> 和 <code>static</code>
迭代器
其他語(yǔ)言中的 Rust
枚舉
詞匯表
If語(yǔ)句
猜猜看
錯(cuò)誤處理
生命周期
編譯器插件
發(fā)布途徑
閉包
trait 對(duì)象
不使用標(biāo)準(zhǔn)庫(kù)
關(guān)聯(lián)常量
外部函數(shù)接口(FFI)
類型轉(zhuǎn)換
原生類型
匹配
參考文獻(xiàn)
Rust 編程語(yǔ)言
內(nèi)聯(lián)匯編
條件編譯
選擇你的保證
學(xué)習(xí) Rust
`type`別名
自定義內(nèi)存分配器
屬性
if let
高效 Rust
可變性
語(yǔ)法和語(yǔ)義
模式
基準(zhǔn)測(cè)試
結(jié)構(gòu)體
變量綁定
語(yǔ)言項(xiàng)
切片模式
<code>Deref</code> 強(qiáng)制多態(tài)
關(guān)聯(lián)類型
裸指針
<code>Borrow</code> 和 <code>AsRef</code>
準(zhǔn)備
Rust 開(kāi)發(fā)版
字符串

選擇你的保證

choosing-your-guarantees.md
commit 6ba952020fbc91bad64be1ea0650bfba52e6aab4

Rust 的一個(gè)重要特性是允許我們控制一個(gè)程序的開(kāi)銷和(安全)保證。

Rust 標(biāo)準(zhǔn)庫(kù)中有多種“wrapper 類型”的抽象,他們代表了大量在開(kāi)銷,工程學(xué)和安全保證之間的權(quán)衡。很多讓你在運(yùn)行時(shí)和編譯時(shí)增強(qiáng)之間選擇。這一部分將會(huì)詳細(xì)解釋一些特定的抽象。

在開(kāi)始之前,強(qiáng)烈建議你閱讀Rust的[所有權(quán)](Ownership 所有權(quán).md)和[借用](References and Borrowing 引用和借用.md)。

基礎(chǔ)指針類型

Box<T>

Box\<T>是一個(gè)“自我擁有的”,或者“裝箱”的指針。因?yàn)樗梢跃S持引用和包含的數(shù)據(jù),它是數(shù)據(jù)的唯一的擁有者。特別的,當(dāng)執(zhí)行類似如下代碼時(shí):

let x = Box::new(1);
let y = x;
// x no longer accessible here

這里,裝箱被移動(dòng)進(jìn)了y。因?yàn)?code>x不再擁有它,此后編譯器不再允許程序猿使用x。相似的一個(gè)函數(shù)可以通過(guò)返回裝箱來(lái)移出函數(shù)。

當(dāng)一個(gè)裝箱(還沒(méi)有被移動(dòng)的)離開(kāi)了作用域,析構(gòu)函數(shù)將會(huì)運(yùn)行。這個(gè)析構(gòu)函數(shù)負(fù)責(zé)釋放內(nèi)部的數(shù)據(jù)。

這是一個(gè)動(dòng)態(tài)分配的零開(kāi)銷抽象。如果你想要在堆上分配一些內(nèi)存并安全的傳遞這些內(nèi)存的指針,這是理想的情況。注意你將只能通過(guò)正常的借用規(guī)則來(lái)共享引用,這些在編譯時(shí)被檢查。

&T&mut T

這分別是不可變和可變引用。他們遵循“讀寫鎖”的模式,也就是你只可能擁有一個(gè)數(shù)據(jù)的可變引用,或者任意數(shù)量的不可變引用,但不是兩者都有。這個(gè)保證在編譯時(shí)執(zhí)行,并且沒(méi)有明顯的運(yùn)行時(shí)開(kāi)銷。在大部分情況這兩個(gè)指針類型有能力在代碼塊之間廉價(jià)的共享引用。

這些指針不能在超出他們的生命周期的情況下被拷貝。

*const T*mut T

這些是C風(fēng)格的指針,并沒(méi)附加生命周期或所有權(quán)。他們只是指向一些內(nèi)存位置,沒(méi)有其他的限制。他們能提供的唯一的保證是除非在標(biāo)記為unsafe的代碼中他們不會(huì)被解引用。

他們?cè)跇?gòu)建像Vec<T>這樣的安全,低開(kāi)銷抽象時(shí)是有用的,不過(guò)應(yīng)該避免在安全代碼中使用。

Rc<T>

這是第一個(gè)我們將會(huì)介紹到的有運(yùn)行時(shí)開(kāi)銷的包裝類型。

Rc\<T>是一個(gè)引用計(jì)數(shù)指針。換句話說(shuō),這讓我們擁有相同數(shù)據(jù)的多個(gè)“有所有權(quán)”的指針,并且數(shù)據(jù)在所有指針離開(kāi)作用域后將被釋放(析構(gòu)函數(shù)將會(huì)執(zhí)行)。

在內(nèi)部,它包含一個(gè)共享的“引用計(jì)數(shù)”(也叫做“refcount”),每次Rc被拷貝時(shí)遞增,而每次Rc離開(kāi)作用域時(shí)遞減。Rc<T>的主要職責(zé)是確保共享的數(shù)據(jù)的析構(gòu)函數(shù)被調(diào)用。

這里內(nèi)部的數(shù)據(jù)是不可變的,并且如果創(chuàng)建了一個(gè)循環(huán)引用,數(shù)據(jù)將會(huì)泄露。如果我們想要數(shù)據(jù)在存在循環(huán)引用時(shí)不被泄漏,我們需要一個(gè)垃圾回收器。

保證

這里(Rc<T>)提供的主要保證是,直到所有引用離開(kāi)作用域后,相關(guān)數(shù)據(jù)才會(huì)被銷毀。

當(dāng)我們想要?jiǎng)討B(tài)分配并在程序的不同部分共享一些(只讀)數(shù)據(jù),且不確定哪部分程序會(huì)最后使用這個(gè)指針時(shí),我們應(yīng)該用Rc<T>。當(dāng)&T不可能靜態(tài)地檢查正確性,或者程序員不想浪費(fèi)時(shí)間編寫反人類的代碼時(shí),它可以作為&T的可行的替代。

這個(gè)指針并不是線程安全的,并且Rust也不會(huì)允許它被傳遞或共享給別的線程。這允許你在不必要的情況下的原子性開(kāi)銷。

Rc<T>有個(gè)姐妹版智能指針類型——Weak<T>。它是一個(gè)既沒(méi)有所有權(quán)、也不能被借用的智能指針。它也比較像&T,但并沒(méi)有生命周期的限制--一個(gè)Weak<T>可以一直存活。然而,嘗試對(duì)其內(nèi)部數(shù)據(jù)進(jìn)行訪問(wèn)可能失敗并返回None,因?yàn)樗梢员扔兴袡?quán)的Rc存活更久。這對(duì)循環(huán)數(shù)據(jù)結(jié)構(gòu)和一些其他類型是有用的。

開(kāi)銷

隨著內(nèi)存使用增加,Rc<T>是一次性的分配,雖然相比一個(gè)常規(guī)Box<T>它會(huì)多分配額外兩個(gè)字(也就是說(shuō),兩個(gè)usize值)。(“強(qiáng)”引用計(jì)數(shù)相比“弱”引用計(jì)數(shù))。

Rc<T>分別在拷貝和離開(kāi)作用域時(shí)會(huì)產(chǎn)生遞增/遞減引用計(jì)數(shù)的計(jì)算型開(kāi)銷。注意拷貝將不會(huì)進(jìn)行一次深度復(fù)制,相反它會(huì)簡(jiǎn)單的遞增內(nèi)部引用計(jì)數(shù)并返回一個(gè)Rc<T>的拷貝。

Cell 類型

Cell提供內(nèi)部可變性。換句話說(shuō),他們包含的數(shù)據(jù)可以被修改,即便是這個(gè)類型并不能以可變形式獲取(例如,當(dāng)他們位于一個(gè)&指針或Rc<T>之后時(shí))。

對(duì)此cell模塊的文檔有一個(gè)非常好的解釋。

這些類型經(jīng)常在結(jié)構(gòu)體字段中出現(xiàn),不過(guò)他們也可能在其他一些地方找到。

Cell<T>

Cell\<T>是一個(gè)提供了零開(kāi)銷內(nèi)部可變性的類型,不過(guò)只用于Copy類型。因?yàn)榫幾g器知道它包含的值對(duì)應(yīng)的所有數(shù)據(jù)都位于棧上,所以并沒(méi)有通過(guò)簡(jiǎn)單的替換數(shù)據(jù)而導(dǎo)致任何位于引用之后的數(shù)據(jù)泄露(或者更糟!)的擔(dān)心。

然而使用這個(gè)封裝仍有可能違反你自己的不可變性,所以謹(jǐn)慎的使用它。它是一個(gè)很好的標(biāo)識(shí),表明一些數(shù)據(jù)塊是可變的并且可能在你第一次讀取它和當(dāng)你想要使用它時(shí)的值并不一樣。

use std::cell::Cell;

let x = Cell::new(1);
let y = &x;
let z = &x;
x.set(2);
y.set(3);
z.set(4);
println!("{}", x.get());

注意這里我們可以通過(guò)多個(gè)不可變的引用改變相同的值。

這與如下代碼有相同的運(yùn)行時(shí)開(kāi)銷:

let mut x = 1;
let y = &mut x;
let z = &mut x;
x = 2;
*y = 3;
*z = 4;
println!("{}", x);

不過(guò)它有額外的優(yōu)勢(shì),它確實(shí)能夠編譯成功。(高級(jí)黑?)

保證

這個(gè)類型放寬了當(dāng)沒(méi)有必要時(shí)“沒(méi)有因可變性導(dǎo)致的混淆”的限制。然而,這也放寬了這個(gè)限制提供的保證;所以當(dāng)你的不可變量依賴存儲(chǔ)在Cell中的數(shù)據(jù),你應(yīng)該多加小心。

這對(duì)改變基本類型和其他Copy類型非常有用,當(dāng)通過(guò)&&mut的靜態(tài)規(guī)則并沒(méi)有其他簡(jiǎn)單合適的方法改變他們的值時(shí)。

Cell并不讓你獲取數(shù)據(jù)的內(nèi)部引用,它讓我們可以自由改變值。

開(kāi)銷

使用Cell<T>并沒(méi)有運(yùn)行時(shí)開(kāi)銷,不過(guò)你使用它來(lái)封裝一個(gè)很大的(Copy)結(jié)構(gòu)體,可能更適合封裝單獨(dú)的字段為Cell<T>因?yàn)槊看螌懭攵紩?huì)是一個(gè)結(jié)構(gòu)體的完整拷貝。

RefCell<T>

RefCell\<T>也提供了內(nèi)部可變性,不過(guò)并不限制為Copy類型。

相對(duì)的,它有運(yùn)行時(shí)開(kāi)銷。RefCell<T>在運(yùn)行時(shí)使用了讀寫鎖模式,不像&T/&mut T那樣在編譯時(shí)執(zhí)行。這通過(guò)borrow()borrow_mut()函數(shù)來(lái)實(shí)現(xiàn),它修改一個(gè)內(nèi)部引用計(jì)數(shù)并分別返回可以不可變的和可變的解引用的智能指針。當(dāng)智能指針離開(kāi)作用域引用計(jì)數(shù)將被恢復(fù)。通過(guò)這個(gè)系統(tǒng),我們可以動(dòng)態(tài)的確保當(dāng)有一個(gè)有效的可變借用時(shí)絕不會(huì)有任何其他有效的借用。如果程序猿嘗試創(chuàng)建一個(gè)這樣的借用,線程將會(huì)恐慌。

use std::cell::RefCell;

let x = RefCell::new(vec![1,2,3,4]);
{
    println!("{:?}", *x.borrow())
}

{
    let mut my_ref = x.borrow_mut();
    my_ref.push(1);
}

Cell相似,它主要用于難以或不可能滿足借用檢查的情況。大體上我們知道這樣的改變不會(huì)發(fā)生在一個(gè)嵌套的形式中,不過(guò)檢查一下是有好處的。

對(duì)于大型的,復(fù)雜的程序,把一些東西放入RefCell來(lái)將事情變簡(jiǎn)單是有用的。例如,Rust編譯器內(nèi)部的ctxt結(jié)構(gòu)體中的很多map都在這個(gè)封裝中。他們只會(huì)在創(chuàng)建時(shí)被修改一次(但并不是正好在初始化后),或者在明顯分開(kāi)的地方多次多次修改。然而,因?yàn)檫@個(gè)結(jié)構(gòu)體被廣泛的用于各個(gè)地方,有效的組織可變和不可變的指針將會(huì)是困難的(也許是不可能的),并且可能產(chǎn)生大量的難以擴(kuò)展的&指針。換句話說(shuō),RefCell提供了一個(gè)廉價(jià)(并不是零開(kāi)銷)的方式來(lái)訪問(wèn)它。之后,如果有人增加一些代碼來(lái)嘗試修改一個(gè)已經(jīng)被借用的cell時(shí),這將會(huì)產(chǎn)生(通常是決定性的)一個(gè)恐慌,并會(huì)被追溯到那個(gè)可惡的借用上。

相似的,在Servo的DOM中有很多可變量,大部分對(duì)于一個(gè)DOM類型都是本地的,不過(guò)有一些交錯(cuò)在DOM中并修改了很多內(nèi)容。使用RefCellCell來(lái)保護(hù)所有的變化可以讓我們免于擔(dān)心到處都是的可變性,并且同時(shí)也表明了何處正在發(fā)生變化。

注意如果是一個(gè)能用&指針的非常簡(jiǎn)單的情形應(yīng)該避免使用RefCell。

保證

RefCell放寬了避免混淆的改變的靜態(tài)限制,并代之以一個(gè)動(dòng)態(tài)限制。保證本身并沒(méi)有改變。

開(kāi)銷

RefCell并不分配空間,不過(guò)它連同數(shù)據(jù)還包含一個(gè)額外的“借用狀態(tài)”指示器(一個(gè)字的大?。?。

在運(yùn)行時(shí)每次借用產(chǎn)生一次引用計(jì)數(shù)的修改/檢查。

同步類型(Synchronous types)

上面的很多類型不能以一種線程安全的方式使用。特別是Rc<T>RefCell<T>,他們都使用非原子的引用計(jì)數(shù)(原子引用計(jì)數(shù)可以在不引起數(shù)據(jù)競(jìng)爭(zhēng)的情況下在多個(gè)線程中遞增),不能在多線程中使用。這讓他們使用起來(lái)更廉價(jià),不過(guò)我們也需要這兩個(gè)類型的線程安全版本。他們以Arc<T>Mutex<T>/RWLock<T>的形式存在。

注意非線程安全的類型不能在線程間傳遞,并且這是在編譯時(shí)檢查的。

Arc<T>

Arc\<T>就是一個(gè)使用原子引用計(jì)數(shù)版本的Rc<T>Atomic reference count,因此是“Arc”)。它可以在線程間自由的傳遞。

C++的shared_ptrArc類似,然而C++的情況中它的內(nèi)部數(shù)據(jù)總是可以改變的。為了語(yǔ)義上與C++的形式相似,我們應(yīng)該使用Arc<Mutex<T>>,Arc<RwLock<T>>,或者Arc<UnsafeCell<T>>1。最后一個(gè)應(yīng)該只被用在我們能確定使用它并不會(huì)造成內(nèi)存不安全性的情況下。記住寫入一個(gè)結(jié)構(gòu)體不是一個(gè)原子操作,并且很多像vec.push()這樣的函數(shù)可以在內(nèi)部重新分配內(nèi)存并產(chǎn)生不安全的行為,所以即便是單一環(huán)境也不足以證明UnsafeCell是安全的。

保證

類似Rc,它提供了當(dāng)最后的Arc離開(kāi)作用域時(shí)(不包含任何的循環(huán)引用)其內(nèi)部數(shù)據(jù)的析構(gòu)函數(shù)將被執(zhí)行的(線程安全的)保證。

開(kāi)銷

使用原子引用計(jì)數(shù)有額外的開(kāi)銷(無(wú)論是被拷貝或者離開(kāi)作用域時(shí)都會(huì)發(fā)生)。當(dāng)在一個(gè)單獨(dú)的線程中通過(guò)一個(gè)Arc共享數(shù)據(jù)時(shí),任何時(shí)候都更傾向于使用&指針。

Mutex<T>RwLock<T>

Mutex\<T>RwLock\<T>通過(guò)RAII guard(guard是一類直到析構(gòu)函數(shù)被調(diào)用時(shí)能保持一些狀態(tài)的對(duì)象)提供了互斥功能。對(duì)于這兩個(gè)類型,mutex直到我們調(diào)用lock()之前它都是無(wú)效的,此時(shí)直到我們獲取鎖這個(gè)線程都會(huì)被阻塞,同時(shí)它會(huì)返回一個(gè)guard。這個(gè)guard可以被用來(lái)訪問(wèn)它的內(nèi)部數(shù)據(jù)(可變的),而當(dāng)guard離開(kāi)作用域鎖將被釋放。

{
    let guard = mutex.lock();
    // guard dereferences mutably to the inner type
    *guard += 1;
} // lock released when destructor runs

RwLock對(duì)多線程讀有額外的效率優(yōu)勢(shì)。只要沒(méi)有writer,對(duì)于共享的數(shù)據(jù)總是可以安全的擁有多個(gè)reader;同時(shí)RwLock讓reader們獲取一個(gè)“讀取鎖”。這樣的鎖可以并發(fā)的獲取并通過(guò)引用計(jì)數(shù)記錄。writer必須獲取一個(gè)“寫入鎖”,它只有在所有reader都離開(kāi)作用域時(shí)才能獲取。

保證

這兩個(gè)類型都提供了線程間安全的共享可變性,然而他們易于產(chǎn)生死鎖。一些額外的協(xié)議層次的安全性可以通過(guò)類型系統(tǒng)獲取。

開(kāi)銷

他們?cè)趦?nèi)部使用類原子類型來(lái)維持鎖,這樣的開(kāi)銷非常大(他們可以阻塞處理器所有的內(nèi)存讀取知道他們執(zhí)行完畢)。而當(dāng)有很多并發(fā)訪問(wèn)時(shí)等待這些鎖也將是很慢的。

組合(Composition)

閱讀Rust代碼時(shí)的一個(gè)常見(jiàn)的痛苦之處是遇到形如Rc<RefCell<Vec<T>>>這樣的類型(或者諸如此類的更復(fù)雜的組合)。這些組合式干什么的,和為什么作者會(huì)選這么一個(gè)類型(以及何時(shí)你應(yīng)該在自己的代碼中使用這樣一個(gè)類型)的理由并不總是顯而易見(jiàn)的。

通常,將你需要的保證組合到一起是一個(gè)例子,而不為無(wú)關(guān)緊要的東西產(chǎn)生開(kāi)銷。

例如,Rc<RefCell<T>>就是一個(gè)這樣的組合。Rc<T>自身并不能可變的解引用;因?yàn)?code>Rc<T>可以共享,而共享的可變性可以導(dǎo)致不安全的行為,所以我們?cè)谄渲蟹湃?code>RefCell<T>來(lái)獲得可以動(dòng)態(tài)驗(yàn)證的共享可變性。現(xiàn)在我們有了共享的可變數(shù)據(jù),不過(guò)它只能以只有一個(gè)writer(沒(méi)有reader)或多個(gè)reader的方式共享。

現(xiàn)在,我們可以更進(jìn)一步,并擁有Rc<RefCell<Vec<T>>>Rc<Vec<RefCell<T>>>,他們都是可共享可改變的vector,不過(guò)他們并不一樣。

前者,RefCell<T>封裝了Vec<T>,所以Vec<T>整體是可變的。與此同時(shí),同一時(shí)刻只能有一個(gè)整個(gè)Vec的可變借用。這意味著你的代碼不能同時(shí)通過(guò)不同的Rc句柄來(lái)操作vector的不同元素。然而,我們可以隨意的從Vec<T>中加入或取出元素。這類似于一個(gè)有運(yùn)行時(shí)借用檢查的&mut Vec<T>。

后者,借用作用于單獨(dú)的元素,不過(guò)vector整體是不可變的。因此,我們可以獨(dú)立的借用不同的元素,不過(guò)我們對(duì)vector加入或取出元素。這類似于&mut [T]2,不過(guò)同樣會(huì)在運(yùn)行時(shí)做借用檢查。

在并發(fā)程序中,我們有一個(gè)使用Arc<Mutex<T>>的類似場(chǎng)景,它提供了共享可變性和所有權(quán)。

當(dāng)閱讀使用這些類型的代碼時(shí),一步步的閱讀并關(guān)注他們提供的保證/開(kāi)銷。

當(dāng)選擇一個(gè)組合類型的時(shí)候,我們必須反過(guò)來(lái)思考;搞清楚我們需要何種保證,以及在組合中的何處我們需要他們。例如,如果面對(duì)一個(gè)Vec<RefCell<T>>RefCell<Vec<T>>之間的選擇,我們需要明確像上面講到的那樣的權(quán)衡并選擇其一。



  1. Arc<UnsafeCell<T>>實(shí)際上并不能編譯因?yàn)?code>UnsafeCell<T>并不是SendSync的,不過(guò)我們可以把它 wrap 進(jìn)一個(gè)類型并且手動(dòng)為其實(shí)現(xiàn)Send/Sync來(lái)獲得Arc<Wrapper<T>>,它的Wrapperstruct Wrapper<T>(UnsafeCell<T>)。 

  2. &[T]&mut [T]切片(slice);他們包含一個(gè)指針和一個(gè)長(zhǎng)度并可以引用一個(gè)vector或數(shù)組的一部分。&mut [T]能夠改變它的元素,不過(guò)長(zhǎng)度不能改變。