午夜视频在线网站,日韩视频精品在线,中文字幕精品一区二区三区在线,在线播放精品,1024你懂我懂的旧版人,欧美日韩一级黄色片,一区二区三区在线观看视频

分享

圖解 Rust 所有權(quán)與生命周期

 新用戶(hù)82165308 2022-08-11 發(fā)布于廣東

后臺(tái)回復(fù)aes13,獲取高清版原圖

1.引言

所有權(quán)生命周期是 Rust 語(yǔ)言非常核心的內(nèi)容。其實(shí)不僅僅是 Rust 有這兩個(gè)概念,在C/C  中也一樣是存在的。而幾乎所有的內(nèi)存安全問(wèn)題也源于對(duì)所有權(quán)和生命周期的錯(cuò)誤使用。只要是不采用垃圾回收來(lái)管理內(nèi)存的程序語(yǔ)言,都會(huì)有這個(gè)問(wèn)題。只是 Rust 在語(yǔ)言級(jí)明確了這兩個(gè)概念,并提供了相關(guān)的語(yǔ)言特性讓用戶(hù)可以顯式控制所有權(quán)的轉(zhuǎn)移與生命周期的聲明。同時(shí)編譯器會(huì)對(duì)各種錯(cuò)誤使用進(jìn)行檢查,提高了程序的內(nèi)存安全性。

所有權(quán)和生命周期其涉及的語(yǔ)言概念很多,本文主要是對(duì)梳理出與“所有權(quán)與生命周期”相關(guān)的概念,并使用 UML 的類(lèi)圖表達(dá)概念間的關(guān)系,幫助更好的理解和掌握。

圖例說(shuō)明

本文附圖都是 UML 類(lèi)圖,UML 類(lèi)圖可以用來(lái)表示對(duì)概念的分析。表達(dá)概念之間的依賴(lài)、繼承、聚合、組成等關(guān)系。圖中的每一個(gè)矩形框都是一個(gè)語(yǔ)義概念,有的是抽象的語(yǔ)言概念,有的是 Rust 庫(kù)中的結(jié)構(gòu)和 Trait。

所有圖中使用的符號(hào)也只有最基礎(chǔ)的幾個(gè)。圖 1 對(duì)符號(hào)體系做簡(jiǎn)單說(shuō)明,主要解釋一下表達(dá)概念之間的關(guān)系的符號(hào)語(yǔ)言。

圖 1 UML 符號(hào)

依賴(lài)關(guān)系:

依賴(lài)是 UML 中最基礎(chǔ)的關(guān)系語(yǔ)義。以帶箭頭的虛線(xiàn)表示,A 依賴(lài)與 B 表達(dá)如下圖。直觀(guān)理解可以是 A “看的見(jiàn)” B,而 B 可以對(duì) A 一無(wú)所知。比如在代碼中 結(jié)構(gòu)體 A 中有 結(jié)構(gòu)體 B 的成員變量,或者 A 的實(shí)現(xiàn)代碼中有 B 的局部變量。這樣如果找不到 B,A 是無(wú)法編譯通過(guò)的。

關(guān)聯(lián)關(guān)系:

一條實(shí)線(xiàn)連接表示兩個(gè)類(lèi)型直接有關(guān)聯(lián),有箭頭表示單向'可見(jiàn)',無(wú)箭頭表示相互之間可見(jiàn)。關(guān)聯(lián)關(guān)系也是一種依賴(lài),但是更具體。有時(shí)候兩個(gè)類(lèi)型之間的關(guān)聯(lián)關(guān)系太復(fù)雜,需要用一個(gè)類(lèi)型來(lái)表達(dá),叫做關(guān)聯(lián)類(lèi)型,如例圖中的 H.

聚合與組成:

聚合與組成都是表示的是整體和部分的關(guān)系。差別在于“聚合”的整體與部分可以分開(kāi),部分可以在多個(gè)整體之間共享。而“組成”關(guān)系中整體對(duì)部分有更強(qiáng)的獨(dú)占性,部分不能被拆開(kāi),部分與整體有相同的生命周期。

繼承與接口實(shí)現(xiàn):

繼承與接口實(shí)現(xiàn)都是一種泛化關(guān)系,C 繼承自 A,表示 A 是更泛化的概念。UML 中各種關(guān)系語(yǔ)義也可以用 UML 自身來(lái)表達(dá),如圖 2:“關(guān)聯(lián)”和“繼承”都是“依賴(lài)”的具體體現(xiàn)方式。

圖 2用 UML表達(dá)UML自身

總圖

圖 3 是本文的總圖,后續(xù)各節(jié)分局部介紹。圖片在布局上很小,點(diǎn)擊放大保存后看。

圖3 所有權(quán)與生命周期總圖

2.所有權(quán)與生命周期期望解決的問(wèn)題

我們從圖中間部分開(kāi)始看起,所謂“所有權(quán)”是指對(duì)一個(gè)變量擁有了一塊“內(nèi)存區(qū)域”。這個(gè)內(nèi)存區(qū)域,可以在堆上,可以在棧上,也可以在代碼段,還有些內(nèi)存地址是直接用于 I/O 地址映射的。這些都是內(nèi)存區(qū)域可能存在的位置。

在高級(jí)語(yǔ)言中,這個(gè)內(nèi)存位置要在程序中要能被訪(fǎng)問(wèn),必然就會(huì)與一個(gè)或多個(gè)變量建立關(guān)聯(lián)關(guān)系(低級(jí)語(yǔ)言如匯編語(yǔ)言,可以直接訪(fǎng)問(wèn)內(nèi)存地址)。也就是說(shuō),通過(guò)這一個(gè)或多個(gè)變量,就能訪(fǎng)問(wèn)這個(gè)內(nèi)存地址。

這就引出三個(gè)問(wèn)題:

  1. 內(nèi)存的不正確訪(fǎng)問(wèn)引發(fā)的內(nèi)存安全問(wèn)題
  2. 由于多個(gè)變量指向同一塊內(nèi)存區(qū)域?qū)е碌臄?shù)據(jù)一致性問(wèn)題
  3. 由于變量在多個(gè)線(xiàn)程中傳遞,導(dǎo)致的數(shù)據(jù)競(jìng)爭(zhēng)的問(wèn)題

由第一個(gè)問(wèn)題引發(fā)的內(nèi)存安全問(wèn)題一般有 5 個(gè)典型情況:

  • 使用未初始化的內(nèi)存
  • 對(duì)空指針解引用
  • 懸垂指針(使用已經(jīng)被釋放的內(nèi)存)
  • 緩沖區(qū)溢出
  • 非法釋放內(nèi)存(釋放未分配的指針或重復(fù)釋放指針)
圖4 變量綁定與內(nèi)存安全的基本概念

這些問(wèn)題在 C/C  中是需要開(kāi)發(fā)者非常小心的自己處理。比如我們可以寫(xiě)一段 C  代碼,把這五個(gè)內(nèi)存安全錯(cuò)誤全部犯一遍。

#include <iostream>

struct Point {
int x;
int y;
};

Point* newPoint(int x,int y) {
Point p { .x=x,.y=y };
return &p; //懸垂指針
}

int main() {
int values[3]= { 1,2,3 };
std::cout<<values[0]<<','<<values[3]<<std::endl; //緩沖區(qū)溢出

Point *p1 = (Point*)malloc(sizeof(Point));
std::cout<<p1->x<<','<<p1->y<<std::endl; //使用未初始化內(nèi)存

Point *p2 = newPoint(10,10); //懸垂指針
delete p2; //非法釋放內(nèi)存

p1 = NULL;
std::cout<<p1->x<<std::endl; //對(duì)空指針解引用
return 0;
}

這段代碼是可以編譯通過(guò)的,當(dāng)然,編譯器還是會(huì)給出警告信息。這段代碼也是可以運(yùn)行的,也會(huì)輸出信息,直到執(zhí)行到最后一個(gè)錯(cuò)誤處“對(duì)空指針解引用時(shí)”才會(huì)發(fā)生段錯(cuò)誤退出。

Rust 的語(yǔ)言特性為上述問(wèn)題提供了解決方案,如下表所示

問(wèn)題解決方案
使用未初始化的內(nèi)存編譯器禁止變量讀取未賦值變量
對(duì)空指針解引用使用 Option 枚舉替代空指針
懸垂指針生命周期標(biāo)識(shí)與編譯器檢查
緩沖區(qū)溢出編譯器檢查,拒絕超越緩沖區(qū)邊界的數(shù)據(jù)訪(fǎng)問(wèn)
非法釋放內(nèi)存語(yǔ)言級(jí)的RAII機(jī)制,只有唯一的所有者才有權(quán)釋放內(nèi)存
多個(gè)變量修改同一塊內(nèi)存區(qū)域允許多個(gè)變量借用所有權(quán),但是同一時(shí)間只允許一個(gè)可變借用
變量在多個(gè)線(xiàn)程中傳遞時(shí)的安全問(wèn)題對(duì)基本數(shù)據(jù)類(lèi)型用 Sync和Send兩個(gè)Trait 標(biāo)識(shí)其線(xiàn)程安全特性,即能否轉(zhuǎn)移所有權(quán)或傳遞可變借用,把這作為基本事實(shí)。再利用泛型限定語(yǔ)法和 Trait impl 語(yǔ)法描述出類(lèi)型線(xiàn)程安全的規(guī)則。編譯期間使用類(lèi)似規(guī)則引擎的機(jī)制,基于基本事實(shí)和預(yù)定義規(guī)則為用戶(hù)代碼中的跨線(xiàn)程數(shù)據(jù)傳遞做推理檢查。

3.變量綁定與所有權(quán)的賦予

Rust 中為什么叫“變量綁定”而不叫“變量賦值'。我們先來(lái)看一段 C  代碼,以及對(duì)應(yīng)的 Rust 代碼。

C :

#include <iostream>

int main()
{
int a = 1;
std::cout << &a << std::endl; /* 輸出 0x62fe1c */
a = 2;
std::cout << &a << std::endl; /* 輸出 0x62fe1c */
}

Rust:

fn main() {
let a = 1;
println!('a:{}',a); // 輸出1
println!('&a:{:p}',&a); // 輸出0x9cf974
//a=2; // 編譯錯(cuò)誤,不可變綁定不能修改綁定的值
let a = 2; // 重新綁定
println!('&a:{:p}',&a); // 輸出0x9cfa14地址發(fā)生了變化
let mut b = 1; // 創(chuàng)建可變綁定
println!('b:{}',b); // 輸出1
println!('&b:{:p}',&b); // 輸出0x9cfa6c
b = 2;
println!('b:{}',b); // 輸出2
println!('&b:{:p}',&b); // 輸出0x9cfa6c地址沒(méi)有變化
let b = 2; // 重新綁定新值
println!('&b:{:p}',&b); // 輸出0x9cfba4地址發(fā)生了變化
}

我們可以看到,在 C  代碼中,變量 a 先賦值為 1,后賦值為 2,但其地址沒(méi)有發(fā)生變化。Rust 代碼中,a 是一個(gè)不可變綁定,執(zhí)行a=2動(dòng)作被編譯器拒絕。但是可以使用 let 重新綁定,但這時(shí) a 的地址跟之前發(fā)生了變化,說(shuō)明 a 被綁定到了另一個(gè)內(nèi)存地址。b 是一個(gè)可變綁定,可以使用b = 2重新給它指向的內(nèi)存賦值,b 的地址不變。但使用 let 重新綁定后,b 指向了新的內(nèi)存區(qū)域。

可以看出,'賦值' 是將值寫(xiě)入變量關(guān)聯(lián)的內(nèi)存區(qū)域,'綁定' 是建立變量與內(nèi)存區(qū)域的關(guān)聯(lián)關(guān)系,Rust 里,還會(huì)把這個(gè)內(nèi)存區(qū)域的所有權(quán)賦予這個(gè)變量。

不可變綁定的含義是:將變量綁定到一個(gè)內(nèi)存地址,并賦予所有權(quán),通過(guò)該變量只能讀取該地址的數(shù)據(jù),不能修改該地址的數(shù)據(jù)。對(duì)應(yīng)的,可變綁定就可以通過(guò)變量修改關(guān)聯(lián)內(nèi)存區(qū)域的數(shù)據(jù)。從語(yǔ)法上看,有 let 關(guān)鍵字是綁定, 沒(méi)有就是賦值。

這里我們能看出 Rust 與 C  的一個(gè)不同之處。C  里是沒(méi)有“綁定”概念的。Rust 的變量綁定概念是一個(gè)很關(guān)鍵的概念,它是所有權(quán)的起點(diǎn)。有了明確的綁定才有了所有權(quán)的歸屬,同時(shí)解綁定的時(shí)機(jī)也確定了資源釋放的時(shí)機(jī)。

所有權(quán)規(guī)則:

  • 每一個(gè)值都有其所有者變量
  • 同一時(shí)間所有者變量只能有一個(gè)
  • 所有者離開(kāi)作用域,值被丟棄(釋放/析構(gòu))

作為所有者,它有如下權(quán)利:

  • 控制資源的釋放
  • 出借所有權(quán)
  • 轉(zhuǎn)移所有權(quán)

4.所有權(quán)的轉(zhuǎn)移

所有者的重要權(quán)利之一就是“轉(zhuǎn)移所有權(quán)”。這引申出三個(gè)問(wèn)題:

  1. 為什么要轉(zhuǎn)移?
  2. 什么時(shí)候轉(zhuǎn)移?
  3. 什么方式轉(zhuǎn)移?

相關(guān)的語(yǔ)言概念如下圖。

圖 5 所有權(quán)轉(zhuǎn)移

為什么要轉(zhuǎn)移所有權(quán)? 我們知道,C/C /Rust 的變量關(guān)聯(lián)了某個(gè)內(nèi)存區(qū)域,但變量總會(huì)在表達(dá)式中進(jìn)行操作再賦值給另一個(gè)變量,或者在函數(shù)間傳遞。實(shí)際上期望被傳遞的是變量綁定的內(nèi)存區(qū)域的內(nèi)容,如果這塊內(nèi)存區(qū)域比較大,復(fù)制內(nèi)存數(shù)據(jù)到給新的變量就是開(kāi)銷(xiāo)很大的操作。所以需要把所有權(quán)轉(zhuǎn)移給新的變量,同時(shí)當(dāng)前變量放棄所有權(quán)。所以歸根結(jié)底,轉(zhuǎn)移所有權(quán)還是為了性能。

所有權(quán)轉(zhuǎn)移的時(shí)機(jī)總結(jié)下來(lái)有以下兩種情況:

  1. 位置表達(dá)式出現(xiàn)在值上下文時(shí)轉(zhuǎn)移所有權(quán)
  2. 變量跨作用域傳遞時(shí)轉(zhuǎn)移所有權(quán)

第一條規(guī)則是一個(gè)精確的學(xué)術(shù)表達(dá),涉及到位置表達(dá)式,值表達(dá)式,位置上下文,值上下文等語(yǔ)言概念。它的簡(jiǎn)單理解就是各種各樣的賦值行為。能明確指向某一個(gè)內(nèi)存區(qū)域位置的表達(dá)式是位置表達(dá)式,其它的都是值表達(dá)式。各種帶有賦值語(yǔ)義的操作的左側(cè)是位置上下文,右側(cè)是值上下文。

當(dāng)位置表達(dá)式出現(xiàn)在值上下文時(shí),其程序語(yǔ)義就是要把這邊位置表達(dá)式所指向的數(shù)據(jù)賦給新的變量,所有權(quán)發(fā)生轉(zhuǎn)移。

第二條規(guī)則是“變量跨作用域時(shí)轉(zhuǎn)移所有權(quán)”。

圖上列舉出了幾種常見(jiàn)的跨作用域行為,能涵蓋大多數(shù)情況,也有簡(jiǎn)單的示例代碼:

  • 變量被花括號(hào)內(nèi)使用
  • match 匹配
  • if let 和 While let
  • 移動(dòng)語(yǔ)義函數(shù)參數(shù)傳遞
  • 閉包捕獲移動(dòng)語(yǔ)義變量
  • 變量從函數(shù)內(nèi)部返回

為什么變量跨作用域要轉(zhuǎn)移所有權(quán)?在 C/C  代碼中,是否轉(zhuǎn)移所有權(quán)是程序員自己隱式或顯式指定的。

試想,在 C/C  代碼中,函數(shù) Fun1 在棧上創(chuàng)建一個(gè) 類(lèi)型 A 的實(shí)例 a, 把它的指針 &a 傳遞給函數(shù) void fun2(A* param) 我們不會(huì)希望 fun2 釋放這個(gè)內(nèi)存,因?yàn)?nbsp;fun1 返回時(shí),棧上的空間會(huì)自動(dòng)被釋放。

如果 fun1 在堆上創(chuàng)建 A 的實(shí)例 a, 把它的指針 &a 傳遞給函數(shù) fun2(A* param),那么關(guān)于 a 的內(nèi)存空間的釋放,fun1 和 fun2 之間需要有個(gè)商量,由誰(shuí)來(lái)釋放。fun1 可能期望由 fun2 來(lái)釋放,如果由 fun2 釋放,則 fun2 并不能判斷這個(gè)指針是在堆上還是棧上。歸根結(jié)底,還是誰(shuí)擁有 a 指向內(nèi)存區(qū)的所有權(quán)問(wèn)題。 C/C  在語(yǔ)言層面上并沒(méi)有強(qiáng)制約束。fun2 函數(shù)設(shè)計(jì)的時(shí)候,需要對(duì)其被調(diào)用的上下文做假定,在文檔中對(duì)對(duì)誰(shuí)釋放這個(gè)變量的內(nèi)存做約定。這樣編譯器實(shí)際上很難對(duì)錯(cuò)誤的使用方式給出警告。

Rust 要求變量在跨越作用域時(shí)明確轉(zhuǎn)移所有權(quán),編譯器可以很清楚作用域邊界內(nèi)外哪個(gè)變量擁有所有權(quán),能對(duì)變量的非法使用作出明確無(wú)誤的檢查,增加的代碼的安全性。

所有權(quán)轉(zhuǎn)移的方式有兩種:

  1. 移動(dòng)語(yǔ)義-執(zhí)行所有權(quán)轉(zhuǎn)移
  2. 復(fù)制語(yǔ)義-不執(zhí)行轉(zhuǎn)移,只按位復(fù)制變量

這里我把 ”復(fù)制語(yǔ)義“定義為所有權(quán)轉(zhuǎn)移的方式之一,也就是說(shuō)“不轉(zhuǎn)移”也是一種轉(zhuǎn)移方式??雌饋?lái)很奇怪。實(shí)際上邏輯是一致的,因?yàn)橛|發(fā)復(fù)制執(zhí)行的時(shí)機(jī)跟觸發(fā)轉(zhuǎn)移的時(shí)機(jī)是一致的。只是這個(gè)數(shù)據(jù)類(lèi)型被打上了 Copy 標(biāo)簽 trait, 在應(yīng)該執(zhí)行轉(zhuǎn)移動(dòng)作的時(shí)候,編譯器改為執(zhí)行按位復(fù)制。

Rust 的標(biāo)準(zhǔn)庫(kù)中為所有基礎(chǔ)類(lèi)型實(shí)現(xiàn)的 Copy Trait。

這里要注意,標(biāo)準(zhǔn)庫(kù)中的

impl<T: ?Sized> Copy for &T {}

為所有引用類(lèi)型實(shí)現(xiàn)了 Copy, 這意味著我們使用引用參數(shù)調(diào)用某個(gè)函數(shù)時(shí),引用變量本身是按位復(fù)制的。標(biāo)準(zhǔn)庫(kù)沒(méi)有為可變借用 &mut T 實(shí)現(xiàn)“Copy” Trait , 因?yàn)榭勺兘栌弥荒苡幸粋€(gè)。后文講閉包捕獲變量的所有權(quán)時(shí)我們可以看到例子。

5.所有權(quán)的借用

變量擁有一個(gè)內(nèi)存區(qū)域所有權(quán),其所有者權(quán)利之一就是“出借所有權(quán)”。

與出借所有權(quán)相關(guān)的概念關(guān)系如圖 6

擁有所有權(quán)的變量借出其所有權(quán)有“引用”和“智能指針”兩種方式:

  1. 引用(包含可變借用和不可變借用)
  2. 智能指針
    • 獨(dú)占式智能指針 Box<T>

    • 非線(xiàn)程安全的引用計(jì)數(shù)智能指針 Rc<T>

    • 線(xiàn)程安全的引用計(jì)數(shù)智能指針 Arc<T>

    • 弱指針 Weak<T>

引用實(shí)際上也是指針,指向的是實(shí)際的內(nèi)存位置。

借用有兩個(gè)重要的安全規(guī)則:

  1. 代表借用的變量,其生命周期不能比被借用的變量(所有者)的生命周期長(zhǎng)

  2. 同一個(gè)變量的可變借用只能有一個(gè)

第一條規(guī)則就是確保不出現(xiàn)“懸垂指針”的內(nèi)存安全問(wèn)題。如果這條規(guī)則被違反,例如:變量 a 擁有存儲(chǔ)區(qū)域的所有權(quán),變量 b 是 a 的某種借用形式,如果 b 的生命周期比 a 長(zhǎng),那么 a 被析構(gòu)后存儲(chǔ)空間被釋放,而 b 仍然可以使用,則 b 就成為了懸垂指針。

第二條是不允許有兩個(gè)可變借用,避免出現(xiàn)數(shù)據(jù)一致性問(wèn)題。

1 Struct Foo{v:i32}
2 fn main(){
3 let mut f = Foo{v:10};
4 let im_ref = &f; // 獲取不可變引用
5 let mut_ref = & mut f; // 獲取可變引用
6 //println!('{}',f.v);
7 //println!('{}',im_ref.v);
8 //println!('{}',mut_ref.v);
9 }

變量 f 擁有值的所有權(quán),im_ref 是其不可變借用,mut_ref 是其可變借用。以上代碼是可以編譯過(guò)去的,但是這幾個(gè)變量都沒(méi)有被使用,這種情況下編譯器并不禁止你同時(shí)擁有可變借用和不可變借用。最后的三行被注釋掉的代碼(6,7,8)使用了這些變量。打開(kāi)一行或多行這些注釋的代碼,編譯器會(huì)報(bào)告不同形式的錯(cuò)誤:

開(kāi)放注釋行編譯器報(bào)告
6正確
7第5行錯(cuò)誤:不能獲得 f 的可變借用,因?yàn)橐呀?jīng)存在不可變借用
8正確
6,7第5行錯(cuò)誤:不能獲得 f 的可變借用,因?yàn)橐呀?jīng)存在不可變借用
6,8第6行錯(cuò)誤:不能獲得 f 的不可變借用,因?yàn)橐呀?jīng)存在可變借用

對(duì)'借用' 的抽象表達(dá)

Rust 的核心包中有兩個(gè)泛型 trait ,core::borrow::Borrow 與 core::borrow::BorrowMut,可以用來(lái)表達(dá)'借用'的抽象含義,分別代表可變借用和不可變借用。前面提到,“借用”有多種表達(dá)形式 (&T,Box<T>,Rc<T> 等等),在不同的使用場(chǎng)景中會(huì)選擇合適的借用表達(dá)方式。它們的抽象形式就可以用 core::borrow::Borrow 來(lái)代表. 從類(lèi)型關(guān)系上, Borrow 是'借用' 概念的抽象形式。從實(shí)際應(yīng)用上,某些場(chǎng)合我們希望獲得某個(gè)類(lèi)型的“借用”,同時(shí)希望能支持所有可能的“借用”形式,Borrow Trait 就有用武之地。

Borrow 的定義如下:

pub trait Borrow<Borrowed: ?Sized> {
fn borrow(&self) -> &Borrowed;
}

它只有一個(gè)方法,要求返回指定類(lèi)型的引用。

Borrow 的文檔中有提供例子

use std::borrow::Borrow;

fn check<T: Borrow<str>>(s: T) {
assert_eq!('Hello', s.borrow());
}

fn main(){
let s: String = 'Hello'.to_string();
check(s);

lets: &str = 'Hello';
check(s);
}

check 函數(shù)的參數(shù)表示它希望接收一個(gè) “str”類(lèi)型的任何形式的“借用”,然后取出其中的值與 “Hello”進(jìn)行比較。

標(biāo)準(zhǔn)庫(kù)中為 String 類(lèi)型實(shí)現(xiàn)了 Borrow<str>,代碼如下

impl Borrow<str> for String{
#[inline]
fn borrow(&self) -> &str{
&self[..]
}
}

所以 String 類(lèi)型可以作為 check 函數(shù)的參數(shù)。

從圖上可以看出,標(biāo)準(zhǔn)庫(kù)為所有類(lèi)型 T 實(shí)現(xiàn)了 Borrow Trait, 也為 &T 實(shí)現(xiàn)了 Borrow Trait。

代碼如下 ,這如何理解。

impl<T: ?Sized> Borrow<T> for T {
fn borrow(&self) -> &T { // 是 fn borrow(self: &Self)的縮寫(xiě),所以 self 的類(lèi)型就是 &T
self
}
}

impl<T: ?Sized> Borrow<T> for &T {
fn borrow(&self) -> &T {
&**self
}
}

這正是 Rust 語(yǔ)言很有意思的地方,非常巧妙的體現(xiàn)了語(yǔ)言的一致性。既然 Borrow<T> 的方法是為了能獲取 T 的引用,那么類(lèi)型 T 和 &T 當(dāng)然也可以做到這一點(diǎn)。在 Borrow for T 的實(shí)現(xiàn)中,

fn borrow(&self)->&T 是 fn borrow(self: &Self)->&T 的縮寫(xiě),所以 self 的類(lèi)型就是 &T,可以直接被返回。在 Borrow for &T 的實(shí)現(xiàn)中,fn borrow(&self)->&T 是 fn borrow(self: &Self)->&T 的縮寫(xiě),所以 self 的類(lèi)型就是 &&T, 需要被兩次解引用得到 T, 再返回其引用。

智能指針 Box<T>,Rc<T>,Arc<T>,都實(shí)現(xiàn)了 Borrow<T> ,其獲取 &T 實(shí)例的方式都是兩次解引用在取引用。Weak<T> 沒(méi)有實(shí)現(xiàn) Borrow<T>, 它需要升級(jí)成 Rc<T> 才能獲取數(shù)據(jù)。

6.生命周期參數(shù)

變量的生命周期主要跟變量的作用域有關(guān),在大部分程序語(yǔ)言中都是隱式定義的。Rust 中能顯式聲明變量的生命周期參數(shù),這是非常獨(dú)特的設(shè)計(jì),其語(yǔ)法特性在其他語(yǔ)言也是不太可能見(jiàn)到的。以下是生命周期概念相關(guān)的圖示。

生命周期參數(shù)的作用

生命周期參數(shù)的核心作用就是解決懸垂指針問(wèn)題。就是讓編譯器幫助檢查變量的生命周期,防止出現(xiàn)變量指向的內(nèi)存區(qū)域被釋放后,變量仍然可以使用的問(wèn)題。那么什么情況下會(huì)讓編譯器無(wú)法判斷生命周期,而必須引入一個(gè)特定語(yǔ)法來(lái)對(duì)生命周期進(jìn)行標(biāo)識(shí)?

我們來(lái)看看最常見(jiàn)的懸垂指針問(wèn)題,函數(shù)以引用方式返回函數(shù)內(nèi)部的局部變量:

struct V{v:i32}

fn bad_fn() -> &V{ //編譯錯(cuò)誤:期望一個(gè)命名的生命周期參數(shù)
let a = V{v:10};
&a
}
let res = bad_fn();

這個(gè)代碼是一個(gè)典型的懸垂指針錯(cuò)誤,a 是函數(shù)內(nèi)的局部變量,函數(shù)返回后 a 就被銷(xiāo)毀,把 a 的引用賦值給 res ,如果能執(zhí)行成功,res 綁定的就是未定義的值。

但編譯器并不是報(bào)告懸垂指針錯(cuò)誤,而是說(shuō)返回類(lèi)型 &V 沒(méi)有指定生命周期參數(shù)。C  的類(lèi)似代碼編譯器會(huì)給出懸垂指針的警告(警告內(nèi)容:局部變量的地址被返回了)。

那我們指定一個(gè)生命周期參數(shù)看看:

fn bad_fn<'a>() -> &'a V{
let a = V{v:10};
let ref_a = &a;
ref_a //編譯錯(cuò)誤:不能返回局部變量的引用
}

這次編譯器報(bào)告的是懸垂指針錯(cuò)誤了。那么編譯器的分析邏輯是什么?

首先我們明確一下 'a 在這里的精確語(yǔ)義到底是什么?

函數(shù)將要返回的引用會(huì)代表一個(gè)內(nèi)存數(shù)據(jù),這個(gè)數(shù)據(jù)有其生命周期范圍,'a 參數(shù)是對(duì)這個(gè)生命周期范圍提出的要求。就像 &V 是對(duì)返回值類(lèi)型提的要求類(lèi)似,'a 是對(duì)返回值生命周期提的要求。編譯器需要檢查的就是實(shí)際返回的數(shù)據(jù),其生命是否符合要求。

那么 'a 參數(shù)對(duì)返回值的生命周期到底提出了什么要求?

我們先區(qū)分一下'函數(shù)上下文'和“調(diào)用者上下文”,函數(shù)上下文是指函數(shù)體內(nèi)部的作用域范圍,調(diào)用者上下文是指該函數(shù)被調(diào)用的位置。上述的懸垂指針錯(cuò)誤其實(shí)并不會(huì)影響函數(shù)上下文范圍的程序執(zhí)行,出問(wèn)題的地方是調(diào)用者上下文拿到一個(gè)無(wú)效引用并使用時(shí),會(huì)出現(xiàn)不可預(yù)測(cè)的錯(cuò)誤。

函數(shù)返回的引用會(huì)在“調(diào)用者上下文”中賦予某個(gè)變量,如:

let res = bod_fn();

res 獲得了返回的引用, 函數(shù)內(nèi)的 ref_a 引用會(huì)按位復(fù)制給變量 res (標(biāo)準(zhǔn)庫(kù)中 impl<T: ?Sized> Copy for &T {} 指定了此規(guī)則)res 會(huì)指向 函數(shù)內(nèi) res_a 同樣的數(shù)據(jù)。為了保證將來(lái)在調(diào)用者上下文不出懸垂指針,編譯器真正要確保的是 res 所指向的數(shù)據(jù)的生命周期,不短于 res 變量自己的生命周期。否則如果數(shù)據(jù)的生命周期短,先被釋放,res 就成為懸垂指針。

可以把這里的 'a 參數(shù)理解為調(diào)用者上下文中接收函數(shù)返回值的變量 res 的生命周期,那么 'a 對(duì)函數(shù)體內(nèi)部返回引用的要求是:返回引用所指代數(shù)據(jù)的生命周期不短于 'a ,也就是不短于調(diào)用者上下文接收返回值的變量的生命周期。

上述例子中函數(shù)內(nèi) ref_a 指代的數(shù)據(jù)生命周期就是函數(shù)作用域,函數(shù)返回前,數(shù)據(jù)被銷(xiāo)毀,生命周期小于調(diào)用者上下文的 res, 編譯器根據(jù) 返回值的生命周期要求與實(shí)際返回值做比較,發(fā)現(xiàn)了錯(cuò)誤。

實(shí)際上,返回的引用或者是靜態(tài)生命周期,或者是根據(jù)函數(shù)輸入的引用參數(shù)通過(guò)運(yùn)算變換得來(lái)的,否則都是這個(gè)結(jié)果,因?yàn)槎际菍?duì)局部數(shù)據(jù)的引用。

靜態(tài)生命周期

看函數(shù)

fn get_str<'a>() -> &'a str {
let s = 'hello';
s
}

這個(gè)函數(shù)可以編譯通過(guò),返回的引用雖然不是從輸入?yún)?shù)推導(dǎo),不過(guò)是靜態(tài)生命周期,可以通過(guò)檢查。

因?yàn)殪o態(tài)生命周期可以理解為“無(wú)窮大”的語(yǔ)義,實(shí)際是跟進(jìn)程的生命周期一致,也就是在程序運(yùn)行期間始終有效。

Rust 的字符串字面量是存儲(chǔ)在程序代碼中,程序加載后在代碼空間,始終有效??梢酝ㄟ^(guò)一個(gè)簡(jiǎn)單試驗(yàn)驗(yàn)證這一點(diǎn):

let s1='Hello';
println!('&s1:{:p}', &s1);//&s1:0x9cf918

let s2='Hello';
println!('&s2:{:p}',&s2);//&s2:0x9cf978
//s1,s2是一樣的值但是地址不一樣,是兩個(gè)不同的引用變量

let ptr1: *const u8 = s1.as_ptr();
println!('ptr1:{:p}', ptr1);//ptr1:0x4ca0a0

let ptr2: *const u8 = s2.as_ptr();
println!('ptr2:{:p}', ptr2);//ptr2:0x4ca0a0

s1,s2 的原始指針都指向同一個(gè)地址,說(shuō)明編譯器為 'Hello' 字面量只保存了一份拷貝,所有引用都指向它。

get_str 函數(shù)中靜態(tài)生命周期長(zhǎng)于返回值要求的'a,所以是合法的。

如果把 get_str 改成

fn get_str<'a>() -> &'static str

即把對(duì)返回值生命周期的要求改為無(wú)窮大,那就只能返回靜態(tài)字符串引用了。

函數(shù)參數(shù)的生命周期

前面的例子為了簡(jiǎn)單起見(jiàn),沒(méi)有輸入?yún)?shù),這并不是一個(gè)典型的情況。大多數(shù)情況下,函數(shù)返回的引用是根據(jù)輸入的引用參數(shù)通過(guò)運(yùn)算變換而來(lái)。比如下面的例子:

fn  remove_prefix<'a>(content:&'a str,prefix:&str) -> &'a str{
if content.starts_with(prefix){
let start:usize = prefix.len();
let end:usize = content.len();
let sub = content.get(start..end).unwrap();
sub
}else{
content
}
}
let s = 'reload';
let sub = remove_prefix(&s0,'re');
println!('{}',sub); // 輸出: load

remove_prefix 函數(shù)從輸入的 content 字符串中判斷是否有 prefix 代表的前綴。如果有就返回 content 不包含前綴的切片,沒(méi)有就返回 content 本身。

無(wú)論如何這個(gè)函數(shù)都不會(huì)返回前綴 prefix ,所以 prefix 變量不需要指定生命周期。

函數(shù)兩個(gè)分支返回的都是通過(guò) content 變量變換出來(lái)的,并作為函數(shù)的返回值。所以 content 必須標(biāo)注生命周期參數(shù),編譯器要根據(jù) content 的生命周期參數(shù)與返回值的要求進(jìn)行比較,判斷是否符合要求。即:實(shí)際返回?cái)?shù)據(jù)的生命周期,大于或等于返回參數(shù)要求的生命周期。

前面說(shuō)到,我們把返回參數(shù)中指定的生命周期參數(shù) 'a 看做調(diào)用者上下文中接收返回值的變量的生命周期,在這個(gè)例子中就是字符串引用 sub,那么輸入?yún)?shù)中的 'a 代表什么意思 ?

這在 Rust 語(yǔ)法設(shè)計(jì)上是一個(gè)很讓人困惑的地方,輸入?yún)?shù)和輸出參數(shù)的生命周期都標(biāo)志為 'a ,似乎是要求兩者的生命周期要求一致,但實(shí)際上并不是這樣。

我們先看看如果輸入?yún)?shù)的生命周期跟輸出參數(shù)期待的不一樣是什么情況,例如下面兩個(gè)例子:

fn echo<'a, 'b>(content: &'b str) -> &'a str {
content //編譯錯(cuò)誤:引用變量本身的生命周期超過(guò)了它的借用目標(biāo)
}
fn longer<'a, 'b>(s1: &'a str, s2: &'b str) -> &'a str {
if s1.len() > s2.len()
{ s1 }
else
{ s2 }//編譯錯(cuò)誤:生命周期不匹配
}

echo 函數(shù)輸入?yún)?shù)生命周期標(biāo)注為 'b , 返回值期待的是 'a .編譯器報(bào)錯(cuò)信息是典型的“懸垂指針”錯(cuò)誤。不過(guò)內(nèi)容似乎并不明確。編譯器指出查閱詳細(xì)信息 --explain E0312 ,這里的解釋是'借用內(nèi)容的生命周期與期待的不一致'。這個(gè)錯(cuò)誤描述就與實(shí)際的錯(cuò)誤情況是相符合的了。

longer 函數(shù)兩個(gè)參數(shù)分別具有生命周期 'a 和 'b , 返回值期待 'a ,當(dāng)返回 s2 時(shí),編譯器報(bào)告生命周期不匹配。把 longer 函數(shù)中的生命周期 'b 標(biāo)識(shí)為比 'a 長(zhǎng),就可以正確編譯了。

fn longer<'a, 'b: 'a>(s1: &'a str, s2: &'b str) -> &'a str {
if s1.len() > s2.len()
{ s1 }
else
{ s2 }//編譯通過(guò)
}

回到我們前面的問(wèn)題,那么輸入?yún)?shù)中的 'a 代表什么意思 ?

我們知道編譯器在函數(shù)定義上下文中所做的生命周期檢查就是要確保”實(shí)際返回?cái)?shù)據(jù)的生命周期,大于或等于返參數(shù)要求的生命周期“。當(dāng)輸入?yún)?shù)給出與返回值一樣的生命周期參數(shù) 'a 時(shí),實(shí)際上是人為地向編譯器保證:在調(diào)用者上下文中,實(shí)際給出的函數(shù)輸入?yún)?shù)的生命周期,不小于將來(lái)用于接收返回值的變量的生命周期。

當(dāng)有兩個(gè)生命周期參數(shù) 'a 'b , 而 'b 大于 'a,當(dāng)然 也保證了在調(diào)用者上下文 'b 代表的輸入?yún)?shù)生命周期也足夠長(zhǎng)。

在函數(shù)定義中,編譯器并不知道將來(lái)實(shí)際調(diào)用這個(gè)函數(shù)的上下文是怎么樣的。生命周期參數(shù)相當(dāng)是函數(shù)上下文與調(diào)用者上下文之間關(guān)于參數(shù)生命周期的協(xié)議。

就像函數(shù)簽名中的類(lèi)型聲明一樣,類(lèi)型聲明約定了與調(diào)用者之間輸入輸出參數(shù)的類(lèi)型,編譯器編譯函數(shù)時(shí),會(huì)檢查函數(shù)體返回的數(shù)據(jù)類(lèi)型與聲明的返回值是否一致。同樣對(duì)與參數(shù)與返回值的生命周期,函數(shù)也會(huì)檢查函數(shù)體中返回的變量生命周期與聲明的是否一致。

前面說(shuō)的是編譯器在“函數(shù)定義上下文的生命周期檢查”機(jī)制,這只是生命周期檢查的一部分,還有另一部分就是“調(diào)用者上下文對(duì)生命周期的檢查”機(jī)制。兩者檢查的規(guī)則如下:

函數(shù)定義上下文的生命周期檢查:

函數(shù)簽名中返回值的生命周期標(biāo)注可以是輸入標(biāo)注的任何一個(gè),只要保證由輸入?yún)?shù)推導(dǎo)出來(lái)的返回的臨時(shí)變量的生命周期,比函數(shù)簽名中返回值標(biāo)注的生命周期相等或更長(zhǎng)。這樣保證了調(diào)用者上下文中,接收返回值的變量,不會(huì)因?yàn)檩斎雲(yún)?shù)失效而成為懸垂指針。

調(diào)用者上下文對(duì)生命周期的檢查:

調(diào)用者上下文中,接收函數(shù)返回借用的變量 res ,其生命周期不能長(zhǎng)于返回的借用的生命周期(實(shí)際是根據(jù)輸入借用參數(shù)推導(dǎo)出來(lái)的)。否則 res 會(huì)在輸入?yún)?shù)失效后成為懸垂指針。

前面 remove_prefix 函數(shù)編譯器已經(jīng)校驗(yàn)合格,那么我們?cè)谡{(diào)用者上下文中構(gòu)建如下例子

let res: &str;
{
let s = String::from('reload');
res = remove_prefix(&s, 're') //編譯錯(cuò)誤:s 的生命周期不夠長(zhǎng)
}
println!('{}', res);

這個(gè)例子中 remove_prefix 被調(diào)用這一行,編譯器會(huì)報(bào)錯(cuò) “s 的生命周期不夠長(zhǎng)”。代碼中的 大括號(hào)創(chuàng)建了一個(gè)新的詞法作用域,導(dǎo)致 res 的生命周期比大括號(hào)內(nèi)部的 s 更長(zhǎng)。這不符合函數(shù)簽名中對(duì)生命周期的要求。函數(shù)簽名要求輸入?yún)?shù)的生命周期不短于返回值要求的生命周期。

結(jié)構(gòu)體定義中的生命周期

結(jié)構(gòu)體中有引用成員時(shí),就會(huì)有潛在的懸垂指針問(wèn)題,需要標(biāo)識(shí)生命周期參數(shù)來(lái)讓編譯器幫助檢查。

struct G<'a>{ m:&'a str}

fn get_g() -> () {
let g: G;
{
let s0 = 'Hi'.to_string();
let s1 = s0.as_str(); //編譯錯(cuò)誤:借用值存活時(shí)間不夠長(zhǎng)
g = G{ m: s1 };
}
println!('{}', g.m);
}

上面的例子中,結(jié)構(gòu)體 G 包含了引用成員,不指定生命周期參數(shù)是無(wú)法編譯的。函數(shù) get_g 演示了在使用者上下文中如何出現(xiàn)生命周期不匹配的情況。

結(jié)構(gòu)體的生命周期定義就是要保證在一個(gè)結(jié)構(gòu)體實(shí)例中,其引用成員的生命周期不短于結(jié)構(gòu)體實(shí)例自身的生命周期。否則如果結(jié)構(gòu)體實(shí)例存活期間,其引用成員的數(shù)據(jù)先被銷(xiāo)毀,那么訪(fǎng)問(wèn)這個(gè)引用成員時(shí)就構(gòu)成了對(duì)懸垂指針的訪(fǎng)問(wèn)。

實(shí)際上結(jié)構(gòu)體的生命周期參數(shù)可以和函數(shù)生命周期參數(shù)做類(lèi)比,成員的生命周期相當(dāng)函數(shù)的輸入?yún)?shù)的生命周期,結(jié)構(gòu)體整體的生命周期相當(dāng)函數(shù)返回值的生命周期。這樣所有之前對(duì)函數(shù)生命周期參數(shù)的分析一樣可以適用。

如果結(jié)構(gòu)體有方法成員會(huì)返回引用參數(shù),方法同樣需要填寫(xiě)生命周期參數(shù)。返回的引用來(lái)源可以是方法的輸入引用參數(shù),也可以是結(jié)構(gòu)體的引用成員。在做生命周期分析的時(shí)候,可以把“方法的輸入引用參數(shù)”和“結(jié)構(gòu)體的引用成員”都看做普通函數(shù)的輸入?yún)?shù),這樣前面對(duì)普通函數(shù)參數(shù)和返回值的生命周期分析方法可以繼續(xù)套用。

泛型的生命周期限定

前文說(shuō)過(guò)生命周期參數(shù)跟類(lèi)型限定很像,比如在代碼

fn longer<'a>(s1:&'a str, s2:&'a str) -> &'a str

struct G<'a>{ m:&'a str }

中,'a 出現(xiàn)的位置參數(shù)類(lèi)型旁邊,一個(gè)對(duì)參數(shù)的靜態(tài)類(lèi)型做限定,一個(gè)對(duì)參數(shù)的動(dòng)態(tài)時(shí)間做限定。'a 使用前需要先聲明,聲明的位置與模板參數(shù)的位置一樣,在 <> 括號(hào)內(nèi),也是用來(lái)放泛型的類(lèi)型參數(shù)的地方。

那么,把類(lèi)型換成泛型可以嗎,語(yǔ)義是什么?使用場(chǎng)景是什么?

我們看看代碼例子:

use std::cmp::Ordering;

#[derive(Eq, PartialEq, PartialOrd, Ord)]
struct G<'a, T:Ord>{ m: &'a T }

#[derive(Eq, PartialEq, PartialOrd, Ord)]
struct Value{ v: i32 }

fn longer<'a, T:Ord>(s1: &'a T, s2: &'a T) -> &'a T {
if s1 > s2 { s1 } else { s2 }
}

fn main(){
let v0 = Value{ v:12 };
let v1 = Value{ v:15 };
let res_v = longer(&v0, &v1);
println!('{}', res_v.v);//15

let g0 = G{ m: &v0 };
let g1 = G{ m: &v1 };
let res_g = longer(&g0, &g1);//15
println!('{}', res_g.m.v);
}

這個(gè)例子擴(kuò)展了 longer 函數(shù),可以對(duì)任何實(shí)現(xiàn)了 Ord trait 的類(lèi)型進(jìn)行操作。 Ord 是核心包中的一個(gè)用于實(shí)現(xiàn)比較操作的內(nèi)置 trait. 這里不細(xì)說(shuō)明。longer 函數(shù)跟前一個(gè)版本比較,只是把 str 類(lèi)型換成了泛型參數(shù) T, 并給 T 增加了類(lèi)型限定 T:Ord.

結(jié)構(gòu)體 G 也擴(kuò)展成可以容納泛型 T,但要求 T 實(shí)現(xiàn)了 Ord trait.

從代碼及執(zhí)行結(jié)果看,跟 把 T 當(dāng)成普通類(lèi)型一樣,沒(méi)有什么特別,生命周期參數(shù)依然是他原來(lái)的語(yǔ)義。

但實(shí)際上 '&'a T' 還隱含另一層語(yǔ)義:如果 T 內(nèi)部含有引用成員,那么其中的引用成員的生命周期要求不短于 T 實(shí)例的生命周期。

老規(guī)矩,我們來(lái)構(gòu)造一個(gè)反例。結(jié)構(gòu)體 G 內(nèi)部包含一個(gè)泛型的引用成員,我們將 G 用于 longer 函數(shù),但是讓 G 內(nèi)部的引用成員生命周期短于 G。代碼如下:

fn main(){
let v0 = Value{ v:12 };
let v1_ref: &Value; // 將 v1 的引用定義在下面大括號(hào)之外,有意延長(zhǎng)變量的生命周期范圍
let res_g: &G<Value>;

{
let v1 = Value{ v:15 };
v1_ref = &v1; //編譯錯(cuò)誤:v1的生命周期不夠長(zhǎng)。
let res_v = longer(&v0,v1_ref);
println!('{}',res_v.v);
}

let g0 = G{ m:&v0 };
let g1 = G{ m:v1_ref }; // 這時(shí)候 v1_ref 已經(jīng)是懸垂指針
res_g = longer(&g0, &g1);
println!('{}', res_g.m.v);
}

變量 g1 自身的生命周期是滿(mǎn)足 longer 函數(shù)要求的,但是其內(nèi)部的引用成員,生命周期過(guò)短。

這個(gè)范例是在“調(diào)用者上下文”檢查時(shí)觸發(fā)的,對(duì)泛型參數(shù)的生命周期限定比較難設(shè)計(jì)出在“函數(shù)定義或結(jié)構(gòu)體定義上下文”觸發(fā)的范例。畢竟 T 只是類(lèi)型指代,定義時(shí)還沒(méi)有具體類(lèi)型。

實(shí)際上要把在 “struct G<'a,T>{m:&'a T}中,T 的所有引用成員的生命周期不短于'a ”這個(gè)語(yǔ)義準(zhǔn)確表達(dá),應(yīng)該寫(xiě)成:

struct G<'a,T:'a>{m:&'a T}

因?yàn)?nbsp;T:'a 才是這個(gè)語(yǔ)義的明確表述。但是第一種表達(dá)方式也是足夠的(我用反證法證明了這一點(diǎn))。所以編譯器也接受第一種比較簡(jiǎn)化的表達(dá)形式。

總而言之,泛型參數(shù)的生命周期限定是兩層含義,一層是泛型類(lèi)型當(dāng)做一個(gè)普通類(lèi)型時(shí)一樣的含義,一層是對(duì)泛型內(nèi)部引用成員的生命周期約束。

Trait 對(duì)象的生命周期

看如下代碼

trait Foo{}
struct Bar{v:i32}
struct Qux<'a>{m:&'a i32}
struct Baz<'a,T>{v:&'a T}

impl Foo for Bar{}
impl<'a> Foo for Qux<'a>{}
impl<'a,T> Foo for Baz<'a,T>{}

結(jié)構(gòu)體 Bar,Qux,Baz 都實(shí)現(xiàn)了 trait Foo, 那么 &Foo 類(lèi)型可以接受這三個(gè)結(jié)構(gòu)體的任何一個(gè)的引用類(lèi)型。

我們把 &Foo 稱(chēng)為 Trait 對(duì)象。

Trait 對(duì)象可以理解為類(lèi)似其它面向?qū)ο笳Z(yǔ)言中,指向接口或基類(lèi)的指針或引用。其它OO語(yǔ)言指向基類(lèi)的指針在運(yùn)行時(shí)確定其實(shí)際類(lèi)型。Rust 沒(méi)有類(lèi)繼承,指向 trait 的指針或引用起到類(lèi)似的效果,運(yùn)行時(shí)被確定具體類(lèi)型。所以編譯期間不知道大小。

Rust 的 Trait 不能有非靜態(tài)數(shù)據(jù)成員,所以 Trait 本身就不會(huì)出現(xiàn)引用成員的生命周期小于對(duì)象自身,所以 Trait 對(duì)象默認(rèn)的生命周期是靜態(tài)生命周期。我們看下面三個(gè)函數(shù):

fn check0() -> &'static Foo { // 如果不指定 'static , 編譯器會(huì)報(bào)錯(cuò),要求指定生命周期命參數(shù), 并建議 'static
const b:Bar = Bar{v:0};
&b
}
fn check1<'a>() -> &'a Foo { //如果不指定 'a , 編譯器會(huì)報(bào)錯(cuò)
const b:Bar = Bar{v:0};
&b
}
fn check2(foo:&Foo) -> &Foo {//生命周期參數(shù)被省略,不要求靜態(tài)生命周期
foo
}
fn check3(foo:&'static Foo) -> &'static Foo {
foo
}
fn main(){
let bar= Bar{v:0};
check2(&bar); //能編譯通過(guò),說(shuō)明 chenk2 的輸入輸出參數(shù)都不是靜態(tài)生命周期
//check3(&bar); //編譯錯(cuò)誤:bar的生命周期不夠長(zhǎng)
const bar_c:Bar =Bar{v:0};
check3(&bar_c); // check3 只能接收靜態(tài)參數(shù)
}

check0 和 check1 說(shuō)明將 Trait 對(duì)象的引用作為 函數(shù)參數(shù)返回時(shí),跟返回其他引用類(lèi)型一樣,都需要指定生命周期參數(shù)。函數(shù) check2 的生命周期參數(shù)只是被省略了(編譯器可以推斷),但這個(gè)函數(shù)里的 Trait 對(duì)象并不是靜態(tài)生命周期,這可以從 main 函數(shù)內(nèi)能成功執(zhí)行 check2(bar) 分析出來(lái),因?yàn)?nbsp;bar 不是靜態(tài)生命周期.

實(shí)際上在運(yùn)行時(shí),Trait 對(duì)象總會(huì)動(dòng)態(tài)綁定到一個(gè)實(shí)現(xiàn)了該 Trait 的具體結(jié)構(gòu)體類(lèi)型(如 Bar,Qux,Baz 等),這個(gè)具體類(lèi)型的在其上下文中有它的生命周期,可以是靜態(tài)的,更多情況下是非靜態(tài)生命周期 'a ,那么 Trait 對(duì)象的生命周期也是 'a.


結(jié)構(gòu)體或成員生命周期Trait 對(duì)象生命周期
Foo無(wú)'static
Bar'a'a
Qux<'a>{m:&'a str}'a'a
Baz<'a,T>{v:&'a T}'a'a
fn qux_update<'a>(qux: &'a mut Qux<'a>, new_value: &'a i32)->&'a Foo {
qux.v = new_value;
qux
}

let value = 100;
let mut qux = Qux{v: &value};
let new_value = 101;
let muted: &dyn Foo = qux_update(& mut qux, &new_value);
qux_update 函數(shù)的智能指針版本如下:

fn qux_box<'a>(new_value: &'a i32) -> Box<Foo 'a> {
Box::new(Qux{v:new_value})
}

let new_value = 101;
let boxed_qux:Box<dyn Foo> = qux_box(&new_value);

返回的智能指針中,Box 裝箱的類(lèi)型包含了引用成員,也需要給被裝箱的數(shù)據(jù)指定生命周期,語(yǔ)法形式是在被裝箱的類(lèi)型位置增加生命周期參數(shù),用 ' ' 號(hào)連接。

這兩個(gè)版本的代碼其實(shí)都說(shuō)明一個(gè)問(wèn)題,就是 Trait 雖然默認(rèn)是靜態(tài)生命周期,但實(shí)際上,其生命周期是由具體實(shí)現(xiàn)這個(gè) Trait 的結(jié)構(gòu)體的生命周期決定,推斷方式跟之前敘述的函數(shù)參數(shù)生命周期并無(wú)太大區(qū)別。

7.智能指針的所有權(quán)與生命周期

如圖 6,在 Rust 中引用和智能指針都算是“指針”的一種形態(tài),所以他們都可以實(shí)現(xiàn) std::borrow::Borrow Trait。一般情況下,我們對(duì)棧中的變量獲取引用,棧中的變量存續(xù)時(shí)間一般比較短,當(dāng)前的作用域退出時(shí),作用域范圍內(nèi)的棧變量就會(huì)被回收。如果我們希望變量的生命周期能跨越當(dāng)前的作用域,甚至在線(xiàn)程之間傳遞,最好是把變量綁定的數(shù)據(jù)區(qū)域創(chuàng)建在堆上。

棧上的變量其作用域在編譯期間就是明確的,所以編譯器能夠確定棧上的變量何時(shí)會(huì)被釋放,結(jié)合生命周期參數(shù)生命,編譯器能找到絕大部分對(duì)棧上變量的錯(cuò)誤引用。

堆上變量其的內(nèi)存管理比棧變量要復(fù)雜很多。在堆上分配一塊內(nèi)存之后,編譯器無(wú)法根據(jù)作用域來(lái)判斷這塊內(nèi)存的存活時(shí)間,必須由使用者顯式指定。C 語(yǔ)言中就是對(duì)于每一塊通過(guò) malloc 分配到的內(nèi)存,需要顯式的使用 free 進(jìn)行釋放。C 中是 new / delete。但是什么時(shí)候調(diào)用 free 或 delete 就是一個(gè)難題。尤其當(dāng)代碼復(fù)雜,分配內(nèi)存的代碼和釋放內(nèi)存的代碼不在同一個(gè)代碼文件,甚至不在同一個(gè)線(xiàn)程的時(shí)候,僅僅靠人工跟蹤代碼的邏輯關(guān)系來(lái)維護(hù)分配與釋放就難免出錯(cuò)。

智能指針的核心思想是讓系統(tǒng)自動(dòng)幫我們決定回收內(nèi)存的時(shí)機(jī)。其主要手段就是“將內(nèi)存分配在堆上,但指向該內(nèi)存的指針變量本身是在棧上,這樣編譯器就可以捕捉指針變量離開(kāi)作用域的時(shí)機(jī)。在這時(shí)決定內(nèi)存回收動(dòng)作,如果該指針變量擁有內(nèi)存區(qū)的所有權(quán)就釋放內(nèi)存,如果是一個(gè)引用計(jì)數(shù)指針就減少計(jì)數(shù)值,計(jì)數(shù)為 0 就回收內(nèi)存”。

Rust 的 Box<T> 為獨(dú)占所有權(quán)指針,Rc<T>為引用計(jì)數(shù)指針,但其計(jì)數(shù)過(guò)程不是線(xiàn)程安全的,Arc<T>提供了線(xiàn)程安全的引用計(jì)數(shù)動(dòng)作,可以跨線(xiàn)程使用。

我們看 Box<T> 的定義

pub struct Box<T: ?Sized>(Unique<T>);
pub struct Unique<T: ?Sized>{
pointer: *const T,
_marker: PhantomData<T>,
}

Box 本身是一個(gè)元組結(jié)構(gòu)體,包裝了一個(gè) Unique<T>, Unique<T>內(nèi)部有一個(gè)原生指針。

(注:Rust 最新版本的 Box 實(shí)現(xiàn)還可以通過(guò)泛型參數(shù)指定內(nèi)存分配器,讓用戶(hù)可以自己控制實(shí)際內(nèi)存的分配。還有為什么通過(guò) Unique多層封裝,這涉及智能指針實(shí)現(xiàn)的具體問(wèn)題,這里不詳述。)

Box 沒(méi)有實(shí)現(xiàn) Copy Trait,它在所有權(quán)轉(zhuǎn)移時(shí)會(huì)執(zhí)行移動(dòng)語(yǔ)意。

示例代碼:

Struct Foo {v:i32}
fn inc(v:& mut Foo) -> &Foo {//省略了生命周期參數(shù)
v.v = v.v 1;
v
}
//返回Box指針不需要生命周期參數(shù),因?yàn)锽ox指針擁有了所有權(quán),不會(huì)成為懸垂指針
fn inc_ptr(mut foo_ptr:Box<Foo>) -> Box<Foo> {//輸入?yún)?shù)和返回參數(shù)各經(jīng)歷一次所有權(quán)轉(zhuǎn)移
foo_ptr.v = foo_ptr.v 1;
println!('ininc_ptr:{:p}-{:p}', &foo_ptr, &*foo_ptr);
foo_ptr
}
fn main() {
let foo_ptr1 = Box::new(Foo{v:10});
println!('foo_ptr1:{:p}-{:p}', &foo_ptr1, &*foo_ptr1);
let mut foo_ptr2 = inc_ptr(foo_ptr1);
//println!('{}',foo_ptr1.v);//編譯錯(cuò)誤,f0_ptr所有權(quán)已經(jīng)丟失
println!('foo_ptr2:{:p}-{:p}', &foo_ptr2, &*foo_ptr2);

inc(foo_ptr2.borrow_mut());//獲得指針內(nèi)數(shù)據(jù)的引用,調(diào)用引用版本的inc函數(shù)
println!('{}',foo_ptr2.v);
}

inc 為引用版本,inc_ptr 是指針版本。改代碼的輸出為:

foo_ptr1:0x8dfad0-0x93a5e0
in inc_ptr:0x8df960-0x93a5e0
foo_ptr2:0x8dfb60-0x93a5e0
12

可以看到 foo_ptr1 進(jìn)入函數(shù) inc_ptr 時(shí),執(zhí)行了一次所有權(quán)轉(zhuǎn)移,函數(shù)返回時(shí)又執(zhí)行了一次。所以三個(gè) Box<Foo> 的變量地址都不一樣,但是它們內(nèi)部的數(shù)據(jù)地址都是一樣的,指向同一個(gè)內(nèi)存區(qū)。

Box 類(lèi)型自身是沒(méi)有引用成員的,但是如果 T 包含引用成員,那么其相關(guān)的生命周期問(wèn)題會(huì)是怎樣的?

我們把 Foo 的成員改成引用成員試試,代碼如下:

use std::borrow::BorrowMut;
struct Foo<'a>{v:&'a mut i32}
fn inc<'a>(foo:&'a mut Foo<'a>) ->&'a Foo<'a> {//生命周期不能省略
*foo.v=*foo.v 1; // 解引用后執(zhí)行加法操作
foo
}
fn inc_ptr(mut foo_ptr:Box<Foo>) -> Box<Foo> {//輸入?yún)?shù)和返回參數(shù)各經(jīng)歷一次所有權(quán)轉(zhuǎn)移
*foo_ptr.v = *foo_ptr.v 1; / 解引用后執(zhí)行加法操作
println!('ininc_ptr:{:p}-{:p}', &foo_ptr, &*foo_ptr);
foo_ptr
}
fn main(){
let mut value = 10;
let foo_ptr1 = Box::new(Foo{v:& mut value});
println!('foo_ptr1:{:p}-{:p}', &foo_ptr1, &*foo_ptr1);
let mut foo_ptr2 = inc_ptr(foo_ptr1);
//println!('{}',foo_ptr1.v);//編譯錯(cuò)誤,f0_ptr所有權(quán)已經(jīng)丟失
println!('foo_ptr2:{:p}-{:p}', &foo_ptr2, &*foo_ptr2);

let foo_ref = inc(foo_ptr2.borrow_mut());//獲得指針內(nèi)數(shù)據(jù)的引用,調(diào)用引用版本的inc函數(shù)
//println!('{}',foo_ptr2.v);//編譯錯(cuò)誤,無(wú)法獲取foo_ptr2.v的不可變借用,因?yàn)橐呀?jīng)存在可變借用
println!('{}', foo_ref.v);
}

引用版本的 inc 函數(shù)生命周期不能再省略了。因?yàn)榉祷?nbsp;Foo 的引用時(shí),有兩個(gè)生命周期值,一個(gè)是Foo 實(shí)例的生命周期,一個(gè)是 Foo 中引用成員的生命周期,編譯器無(wú)法做推斷,需要指定。但是智能指針版本 inc_ptr 函數(shù)的生命周期依然不用指定。Foo 的實(shí)例被智能指針包裝,生命周期由 Box 負(fù)責(zé)管理。

如果 Foo 是一個(gè) Trait ,而實(shí)現(xiàn)它的結(jié)構(gòu)體有引用成員,那么 Box<Foo> 的生命周期會(huì)有什么情況。示例代碼如下:

trait Foo{
fn inc(&mut self);
fn value(&self)->i32;
}

struct Bar<'a>{v:&'amuti32}

impl<'a> Foo for Bar<'a> {
fn inc(&mutself){
*(self.v)=*(self.v) 1
}
fn value(&self)->i32{
*self.v
}
}

fn inc(foo:& mut dyn Foo)->& dyn Foo {//生命周期參數(shù)被省略
foo.inc();
foo
}

fn inc_ptr(mut foo_ptr:Box<dyn Foo>) -> Box< dyn Foo> {//輸入?yún)?shù)和返回參數(shù)各經(jīng)歷一次所有權(quán)轉(zhuǎn)移
foo_ptr.inc();
foo_ptr
}

fn main() {
}

引用版本和智能指針版本都沒(méi)生命周期參數(shù),可以編譯通過(guò)。不過(guò) main 函數(shù)里是空的,也就是沒(méi)有使用這些函數(shù),只是定義編譯通過(guò)了。我先試試使用引用版本:

fn main(){
let mut value = 10;
let mut foo1= Bar{v:& mut value};
let foo2 =inc(&mut foo1);
println!('{}', foo2.value()); // 輸出 11
}

可以編譯通過(guò)并正常輸出。再試智能指針版本:

fn main(){
let mut value = 10;
let foo_ptr1 = Box::new(Bar{v:&mut value}); //編譯錯(cuò)誤:value生命周期太短
let mut foo_ptr2 = inc_ptr(foo_ptr1); //編譯器提示:類(lèi)型轉(zhuǎn)換需要value為靜態(tài)生命周期
}

編譯失敗。提示的錯(cuò)誤信息是 value 的生命周期太短,需要為 'static 。因?yàn)?nbsp;Trait 對(duì)象( Box< dyn Foo>)默認(rèn)是靜態(tài)生命周期,編譯器推斷出返回?cái)?shù)據(jù)的生命周期太短。去掉最后一行 inc_ptr 是可以正常編譯的。

如果將 inc_ptr 的定義加上生命周期參數(shù)上述代碼就可以編譯通過(guò)。修改后的 inc_ptr 如下:

fn inc_ptr<'a>(mut foo_ptr:Box<dyn Foo 'a>) -> Box<dyn Foo 'a> {
foo_ptr.inc();
foo_ptr
}

為什么指針版本不加生命周期參數(shù)會(huì)出錯(cuò),而引用版沒(méi)有生命周期參數(shù)卻沒(méi)有問(wèn)題?

因?yàn)橐冒媸鞘÷粤松芷趨?shù),完整寫(xiě)法是:

fn inc<'a>(foo:&'a mut dyn Foo)->&'a dyn Foo {
foo.inc();
foo
}

8. 閉包與所有權(quán)

這里不介紹閉包的使用,只說(shuō)與所有權(quán)相關(guān)的內(nèi)容。閉包與普通函數(shù)相比,除了輸入?yún)?shù),還可以捕獲上線(xiàn)文中的變量。閉包還支持一個(gè) move 關(guān)鍵字,來(lái)強(qiáng)制轉(zhuǎn)移捕獲變量的所有權(quán)。

我們先來(lái)看 move 對(duì)輸入?yún)?shù)有沒(méi)有影響:

//結(jié)構(gòu) Value 沒(méi)有實(shí)現(xiàn)Copy Trait
struct Value{x:i32}

//沒(méi)有作為引用傳遞參數(shù),所有權(quán)被轉(zhuǎn)移
let mut v = Value{x:0};
let fun = |p:Value| println!('in closure:{}', p.x);
fun(v);
//println!('callafterclosure:{}',point.x);//編譯錯(cuò)誤:所有權(quán)已經(jīng)丟失

//作為閉包的可變借用入?yún)?,閉包定義沒(méi)有move,所有權(quán)沒(méi)有轉(zhuǎn)移
let mut v = Value{x:0};
let fun = |p:&mut Value| println!('in closure:{}', p.x);
fun(& mut v);
println!('call after closure:{}', v.x);

//可變借用作為閉包的輸入?yún)?shù),閉包定義增加move,所有權(quán)沒(méi)有轉(zhuǎn)移
let mut v = Value{x:0};
let fun = move |p:& mut Value| println!('in closure:{}', p.x);
fun(& mut v);
println!('call after closure:{}', v.x);

可以看出,變量作為輸入?yún)?shù)傳遞給閉包時(shí),所有權(quán)轉(zhuǎn)移規(guī)則跟普通函數(shù)是一樣的,move 關(guān)鍵字對(duì)閉包輸入?yún)?shù)的引用形式不起作用,輸入?yún)?shù)的所有權(quán)沒(méi)有轉(zhuǎn)移。

對(duì)于閉包捕獲的上下文變量,所有權(quán)是否轉(zhuǎn)移就稍微復(fù)雜一些。

下表列出了 10 多個(gè)例子,每個(gè)例子跟它前后的例子都略有不同,分析這些差別,我們能得到更清晰的結(jié)論。

首先要明確被捕獲的變量是哪個(gè),這很重要。比如例 8 中,ref_v 是 v 的不可變借用,閉包捕獲的是 ref_v ,那么所有權(quán)轉(zhuǎn)移的事情跟 v 沒(méi)有關(guān)系,v 不會(huì)發(fā)生與閉包相關(guān)的所有權(quán)轉(zhuǎn)移事件。

明確了被捕獲的變量后,是否轉(zhuǎn)移所有權(quán)受三個(gè)因素聯(lián)合影響:

  1. 變量被捕獲的方式(值,不可變借用,可變借用)

  2. 閉包是否有 move 限定

  3. 被捕獲變量的類(lèi)型是否實(shí)現(xiàn)了 'Copy' Trait

是用偽代碼描述是否轉(zhuǎn)移所有權(quán)的規(guī)則如下:

if 捕獲方式 == 值傳遞 {
if 被捕獲變量的類(lèi)型實(shí)現(xiàn)了 'Copy'
不轉(zhuǎn)移所有權(quán) // 例 :9
else
轉(zhuǎn)移所有權(quán) // 例 :1
}
}
else { // 捕獲方式是借用
if 閉包沒(méi)有 move 限定
不轉(zhuǎn)移所有權(quán) // 例:2,3,6,10,12
else { // 有 move
if 被捕獲變量的類(lèi)型實(shí)現(xiàn)了 'Copy'
不轉(zhuǎn)移所有權(quán) // 例: 8
else
轉(zhuǎn)移所有權(quán) // 例: 4,5,7,11,13,14
}
}

先判斷捕獲方式,如果是值傳遞,相當(dāng)于變量跨域了作用域,觸發(fā)轉(zhuǎn)移所有權(quán)的時(shí)機(jī)。move 是對(duì)借用捕獲起作用,要求對(duì)借用捕獲也觸發(fā)所有權(quán)轉(zhuǎn)移。是否實(shí)現(xiàn) 'Copy' 是最后一步判斷。前文提到,我們可以把 Copy Trait 限定的位拷貝語(yǔ)義當(dāng)成一種轉(zhuǎn)移執(zhí)行的方式。Copy Trait 不參與轉(zhuǎn)移時(shí)機(jī)的判定,只在最后轉(zhuǎn)移執(zhí)行的時(shí)候起作用。

  • 例 1 和(例 2、例 3) 的區(qū)別在于捕獲方式不同。

  • (例 2、例 3) 和例 4 的區(qū)別在于 move 關(guān)鍵字。

  • 例 6 和例 7 的區(qū)別 演示了 move 關(guān)鍵字對(duì)借用方式捕獲的影響。

  • 例 8 說(shuō)明了捕獲不可變借用變量,無(wú)論如何都不會(huì)轉(zhuǎn)移,因?yàn)椴豢勺兘栌脤?shí)現(xiàn)了 Copy.

  • 例 8 和例 11 的區(qū)別就在于例 11 捕獲的 '不可變借用'沒(méi)有實(shí)現(xiàn) 'Copy' Trait 。

  • 例 10 和例 11 是以“不可變借用的方式”捕獲了一個(gè)“可變借用變量”

  • 例 12,13,14 演示了對(duì)智能指針的效果,判斷邏輯也是一致的。

C 11 的閉包需要在閉包聲明中顯式指定是按值還是按引用捕獲,Rust 不一樣。Rust 閉包如何捕獲上下文變量,不取決與閉包的聲明,取決于閉包內(nèi)部如何使用被捕獲的變量。實(shí)際上編譯器會(huì)盡可能以借用的方式去捕獲變量(例,除非實(shí)在不行,如例 1.)

這里刻意沒(méi)有提及閉包背后的實(shí)現(xiàn)機(jī)制,即 Fn,FnMut,FnOnce 三個(gè) Trait。因?yàn)槲覀冎挥瞄]包語(yǔ)法時(shí)是看不到編譯器對(duì)閉包的具體實(shí)現(xiàn)的。所以我們僅從閉包語(yǔ)法本身去判斷所有權(quán)轉(zhuǎn)移的規(guī)則。

9.多線(xiàn)程環(huán)境下的所有權(quán)問(wèn)題

我們把前面的例 1 再改一下,上下文與閉包的實(shí)現(xiàn)都沒(méi)有變化,但是閉包在另一個(gè)線(xiàn)程中執(zhí)行。

let v = Value{x:1};
let child = thread::spawn(||{ // 編譯器報(bào)錯(cuò),要求添加 move 關(guān)鍵字
let p = v;
println!('inclosure:{}',p.x)
});
child.join();

這時(shí),編譯器報(bào)錯(cuò),要求給閉包增加 move 關(guān)鍵字。也就是說(shuō),閉包作為線(xiàn)程的入口函數(shù)時(shí),強(qiáng)制要求對(duì)被捕獲的上下文變量執(zhí)行移動(dòng)語(yǔ)義。下面我們看看多線(xiàn)程環(huán)境下的所有權(quán)系統(tǒng)。

前面的討論都不涉及變量在跨線(xiàn)程間的共享,一旦多個(gè)線(xiàn)程可以訪(fǎng)問(wèn)同一個(gè)變量時(shí),情況又復(fù)雜了一些。這里有兩個(gè)問(wèn)題,一個(gè)仍然是內(nèi)存安全問(wèn)題,即“懸垂指針”等 5 個(gè)典型的內(nèi)存安全問(wèn)題,另一個(gè)是線(xiàn)程的執(zhí)行順序?qū)е聢?zhí)行結(jié)果不可預(yù)測(cè)的問(wèn)題。這里我們只關(guān)注內(nèi)存安全問(wèn)題。

首先,多個(gè)線(xiàn)程如何共享變量?前面的例子演示了啟動(dòng)新線(xiàn)程時(shí),通過(guò)閉包捕獲上下文中的變量來(lái)實(shí)現(xiàn)多個(gè)線(xiàn)程共享變量。這是一個(gè)典型的形式,我們以這個(gè)形式為基礎(chǔ)來(lái)闡述多線(xiàn)程環(huán)境下的所有權(quán)問(wèn)題。

我們來(lái)看例子代碼:

//結(jié)構(gòu) Value 沒(méi)有實(shí)現(xiàn)Copy Trait
struct Value{x:i32}

let v = Value{x:1};
let child = thread::spawn(move||{
let p = v;
println!('in closure:{}',p.x)
});
child.join();
//println!('{}',v.x);//編譯錯(cuò)誤:所有權(quán)已經(jīng)丟失

這是前面例子的正確實(shí)現(xiàn),變量 v 被傳遞到另一個(gè)線(xiàn)程(閉包內(nèi)),執(zhí)行了所有權(quán)轉(zhuǎn)移

//閉包捕獲的是一個(gè)引用變量,無(wú)論如何也拿不到所有權(quán)。那么多線(xiàn)程環(huán)境下所有引用都可以這么傳遞嗎?
let v = Value{x:0};
let ref_v = &v;
let fun = move ||{
let p = ref_v;
println!('inclosure:{}',p.x)
};
fun();
println!('call after closure:{}',v.x);//編譯執(zhí)行成功

這個(gè)例子中,閉包捕獲的是一個(gè)變量的引用,Rust 的引用都是實(shí)現(xiàn)了 Copy Trait,會(huì)被按位拷貝到閉包內(nèi)的變量 p.p 只是不可變借用,沒(méi)有獲得所有權(quán),但是變量 v 的不可變借用在閉包內(nèi)外進(jìn)行了傳遞。那么把它改成多線(xiàn)程方式會(huì)如何呢?這是多線(xiàn)程下的實(shí)現(xiàn)和編譯器給出的錯(cuò)誤提示:

let  v:Value = Value{x:1};
let ref_v = &v; // 編譯錯(cuò)誤:被借用的值 v0 生命周期不夠長(zhǎng)
let child = thread::spawn(move||{
let p = ref_v;
println!('in closure:{}',p.x)
}); // 編譯器提示:參數(shù)要求 v0 被借用時(shí)為 'static 生命周期
child.join();

編譯器的核心意思就是 v 的生命周期不夠長(zhǎng)。當(dāng) v 的不可變借用被傳遞到閉包中,并在另一個(gè)線(xiàn)程中使用時(shí),主線(xiàn)程繼續(xù)執(zhí)行, v 隨時(shí)可能超出作用域范圍被回收,那么子線(xiàn)程中的引用變量就變成了懸垂指針。如果 v 為靜態(tài)生命周期,這段代碼就可以正常編譯執(zhí)行。即把第一行改為:

const v:Value = Value{x:1};

當(dāng)然只能傳遞靜態(tài)生命周期的引用實(shí)際用途有限,多數(shù)情況下我們還是希望能把非靜態(tài)的數(shù)據(jù)傳遞給另一個(gè)線(xiàn)程??梢圆捎?nbsp;Arc<T>來(lái)包裝數(shù)據(jù)。 Arc<T> 是引用計(jì)數(shù)的智能指針,指針計(jì)數(shù)的增減操作是線(xiàn)程安全的原子操作,保證計(jì)數(shù)的變化是線(xiàn)程安全的。

//線(xiàn)程安全的引用計(jì)數(shù)智能指針Arc可以在線(xiàn)程間傳遞
let v1 = Arc::new(Value{x:1});
let arc_v = v1.clone();
let child = thread::spawn(move||{
let p = arc_v;
println!('Arc<Value>in closure:{}',p.x)
});
child.join();
//println!('Arc<Value> out of closure:{}',arc_v.x);//編譯錯(cuò)誤,克隆出來(lái)的指針變量的所有權(quán)丟失

如果把上面的 Arc<T> 換成 Rc<T> ,編譯器會(huì)報(bào)告錯(cuò)誤,說(shuō)'Rc<T> 不能在線(xiàn)程間安全的傳遞'。

通過(guò)上面的例子我們可以總結(jié)出來(lái)一點(diǎn),因?yàn)殚]包定義中的 move 關(guān)鍵字,以閉包啟動(dòng)新線(xiàn)程時(shí),被閉包捕獲的變量本身的所有權(quán)必然會(huì)發(fā)生轉(zhuǎn)移。無(wú)論捕獲的變量是 '值變量'還是引用變量或智能指針(上述例子中 v,ref_v,arc_v 本身的所有權(quán)被轉(zhuǎn)移)。但是對(duì)于引用或指針,它們所指代的數(shù)據(jù)的所有權(quán)并不一定被轉(zhuǎn)移。

那么對(duì)于上面的類(lèi)型 struct Value{x:i32}它的值可以在多個(gè)線(xiàn)程間傳遞(轉(zhuǎn)移所有權(quán)),它的多個(gè)不可變借用可以在多個(gè)線(xiàn)程間同時(shí)存在。同時(shí) &Value 和 Arc<Value> 可以在多個(gè)線(xiàn)程間傳遞(轉(zhuǎn)移引用變量或指針變量自身的所有權(quán)),但是 Rc<T> 不行。

要知道,Rc<T> 和 Arc<T> 只是 Rust 標(biāo)準(zhǔn)庫(kù)(std)實(shí)現(xiàn)的,甚至不在核心庫(kù)(core)里。也就是說(shuō),它們并不是 Rust 語(yǔ)言機(jī)制的一部分。那么,編譯器是如何來(lái)判斷 Arc 可以安全的跨線(xiàn)程傳遞,而 Rc 不行呢?

Rust 核心庫(kù) 的 marker.rs 文件中定義了兩個(gè)標(biāo)簽 Trait:

pub unsafe auto trait Sync{}
pub unsafe auto trait Send{}

標(biāo)簽 Trait 的實(shí)現(xiàn)是空的,但編譯器會(huì)分析某個(gè)類(lèi)型是否實(shí)現(xiàn)了這個(gè)標(biāo)簽 Trait.

  • 如果一個(gè)類(lèi)型 T實(shí)現(xiàn)了“Sync”,其含義是 T 可以安全的通過(guò)引用可以在多個(gè)線(xiàn)程間被共享。
  • 如果一個(gè)類(lèi)型 T實(shí)現(xiàn)了“Send”,其含義是 T 可以安全的跨線(xiàn)程邊界被傳遞。

那么上面的例子中的類(lèi)型,Value ,&Value,Arc<Value> 類(lèi)型一定都實(shí)現(xiàn)了“SendTrait. 我們看看如何實(shí)現(xiàn)的。

marker.rs 文件還定義了兩條規(guī)則:

unsafe impl<T:Sync   ?Sized> Send for &T{}
unsafe impl<T:Send ?Sized> Send for & mut T{}

其含義分別是:

  • 如果類(lèi)型 T 實(shí)現(xiàn)了“Sync”,則自動(dòng)為類(lèi)型 &T 實(shí)現(xiàn)“Send”.
  • 如果類(lèi)型 T 實(shí)現(xiàn)了“Send”,則自動(dòng)為類(lèi)型 &mut T 實(shí)現(xiàn)“Send”.

這兩條規(guī)則都可以直觀(guān)的理解。比如:對(duì)第一條規(guī)則 T 實(shí)現(xiàn)了 “Sync”, 意味則可以在很多個(gè)線(xiàn)程中出現(xiàn)同一個(gè) T 實(shí)例的 &T 類(lèi)型實(shí)例。如果線(xiàn)程 A 中先有 &T 實(shí)例,線(xiàn)程 B 中怎么得到 &T 的實(shí)例呢?必須要有在線(xiàn)程 A 中通過(guò)某種方式 send 過(guò)來(lái),比如閉包的捕獲上下文變量。而且 &T 實(shí)現(xiàn)了 'CopyTrait, 不會(huì)有所有權(quán)風(fēng)險(xiǎn),數(shù)據(jù)是只讀的不會(huì)有數(shù)據(jù)競(jìng)爭(zhēng)風(fēng)險(xiǎn),非常安全。邏輯上也是正確的。那為什么還會(huì)別標(biāo)記為 unsafe ? 我們先把這個(gè)問(wèn)題暫時(shí)擱置,來(lái)看看為智能指針設(shè)計(jì)的另外幾條規(guī)則。

impl <T:?Sized>!marker::Send for Rc<T>{}
impl <T:?Sized>!marker::Sync for Rc<T>{}
impl<T:?Sized>!marker::Send for Weak<T>{}
impl<T:?Sized>!marker::Sync for Weak<T>{}
unsafe impl<T:?Sized Sync Send>Send for Arc<T>{}
unsafe impl<T:?Sized Sync Send>Sync for Arc<T>{}

這幾條規(guī)則明確指定 Rc<T> 和 Weak<T> 不能實(shí)現(xiàn) “Sync”和 “Send”。

同時(shí)規(guī)定如果類(lèi)型 T 實(shí)現(xiàn)了 “Sync”和 “Send”,則自動(dòng)為 Arc<T> 實(shí)現(xiàn) “Sync”和 “Send”。Arc<T> 對(duì)引用計(jì)數(shù)增減是原子操作,所以它的克隆體可以在多個(gè)線(xiàn)程中使用(即可以為 Arc<T> 實(shí)現(xiàn)”Sync”和“Send”),但為什么其前提條件是要求 T 也要實(shí)現(xiàn)'Sync”和 “Send”呢。

我們知道,Arc<T>實(shí)現(xiàn)了 std::borrow,可以通過(guò) Arc<T>獲取 &T 的實(shí)例,多個(gè)線(xiàn)程中的 Arc<T> 實(shí)例當(dāng)然也可以獲取到多個(gè)線(xiàn)程中的 &T 實(shí)例,這就要求 T 必須實(shí)現(xiàn)“Sync”。Arc<T> 是引用計(jì)數(shù)的智能指針,任何一個(gè)線(xiàn)程中的 Arc<T>的克隆體都有可能成為最后一個(gè)克隆體,要負(fù)責(zé)內(nèi)存的釋放,必須獲得被 Arc<T>指針包裝的 T 實(shí)例的所有權(quán),這就要求 T 必須能跨線(xiàn)程傳遞,必須實(shí)現(xiàn) “Send”。

Rust 編譯器并沒(méi)有為 Rc<T>或 Arc<T> 做特殊處理,甚至在語(yǔ)言級(jí)并不知道它們的存在,編譯器本身只是根據(jù)類(lèi)型是否實(shí)現(xiàn)了 “Sync”和 “Send”標(biāo)簽來(lái)進(jìn)行推理。實(shí)際上可以認(rèn)為編譯器實(shí)現(xiàn)了一個(gè)檢查變量跨線(xiàn)程傳遞安全性的規(guī)則引擎,編譯器為基本類(lèi)型直接實(shí)現(xiàn) “Sync”和 “Send”,這作為“公理”存在,然后在標(biāo)準(zhǔn)庫(kù)代碼中增加一些“定理”,也就是上面列舉的那些規(guī)則。用戶(hù)自己實(shí)現(xiàn)的類(lèi)型可以自己指定是否實(shí)現(xiàn) “Sync”和 “Send”,多數(shù)情況下編譯器會(huì)根據(jù)情況默認(rèn)選擇是否實(shí)現(xiàn)。代碼編譯時(shí)編譯器就可以根據(jù)這些公理和規(guī)則進(jìn)行推理。這就是 Rust 編譯器支持跨線(xiàn)程所有權(quán)安全的秘密。

對(duì)于規(guī)則引擎而言,'公理'和'定理'是不言而喻無(wú)需證明的,由設(shè)計(jì)者自己聲明,設(shè)計(jì)者自己保證其安全性,編譯器只保證只要定理和公理沒(méi)錯(cuò)誤,它的推理也沒(méi)錯(cuò)誤。所以的'公理'和'定理'都標(biāo)注為 unsafe,提醒聲明著檢查其安全性,用戶(hù)也可以定義自己的'定理',有自己保證安全。反而否定類(lèi)規(guī)則 (實(shí)現(xiàn) !Send 或 !Sync)不用標(biāo)注為 unsafe , 因?yàn)樗鼈冎苯泳芙^了變量跨線(xiàn)程傳遞,沒(méi)有安全問(wèn)題。

當(dāng)編譯器確定 “Sync”和 “Send”適合某個(gè)類(lèi)型時(shí),會(huì)自動(dòng)為其實(shí)現(xiàn)此。

比如編譯器默認(rèn)為以下類(lèi)型實(shí)現(xiàn)了 Sync :

  • [u8] 和 [f64] 這樣的基本類(lèi)型都是 [Sync],

  • 包含它們的簡(jiǎn)單聚合類(lèi)型(如元組、結(jié)構(gòu)和名號(hào))也是[Sync] 。

  • '不可變' 類(lèi)型(如 &T)

  • 具有簡(jiǎn)單繼承可變性的類(lèi)型,如 Box 、Vec

  • 大多數(shù)其他集合類(lèi)型(如果泛型參數(shù)是 [Sync],其容器就是 [Sync]。

用戶(hù)也可以手動(dòng)使用 unsafe 的方式直接指定。

下圖是與跨線(xiàn)程所有權(quán)相關(guān)的概念和類(lèi)型的 UML 圖。

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶(hù)發(fā)布,不代表本站觀(guān)點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買(mǎi)等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶(hù) 評(píng)論公約

    類(lèi)似文章 更多