生命周期

生命周期( Lifetime )

下面是一個資源借用的例子:

fn main() {

? ? let a = 100_i32;

? ? {

? ? ? ? let x = &a;

? ? }? // x 作用域結(jié)束

? ? println!("{}", x);

}

編譯時,我們會看到一個嚴(yán)重的錯誤提示:

error: unresolved name?x.

錯誤的意思是“無法解析?x?標(biāo)識符”,也就是找不到?x?, 這是因為像很多編程語言一樣,Rust中也存在作用域概念,當(dāng)資源離開離開作用域后,資源的內(nèi)存就會被釋放回收,當(dāng)借用/引用離開作用域后也會被銷毀,所以?x?在離開自己的作用域后,無法在作用域之外訪問。

上面的涉及到幾個概念:

Owner: 資源的所有者?a

Borrower: 資源的借用者?x

Scope: 作用域,資源被借用/引用的有效期

強(qiáng)調(diào)下,無論是資源的所有者還是資源的借用/引用,都存在在一個有效的存活時間或區(qū)間,這個時間區(qū)間稱為生命周期, 也可以直接以Scope作用域去理解。

所以上例子代碼中的生命周期/作用域圖示如下:

? ? ? ? ? ? {? ? a? ? {? ? x? ? }? ? *? ? }

所有者 a:? ? ? ? |________________________|

借用者 x:? ? ? ? ? ? ? ? ? |____|? ? ? ? ? ? x = &a

? 訪問 x:? ? ? ? ? ? ? ? ? ? ? ? ? ? |? ? ? 失?。涸L問 x

可以看到,借用者?x?的生命周期是資源所有者?a?的生命周期的子集。但是?x?的生命周期在第一個?}?時結(jié)束并銷毀,在接下來的?println!?中再次訪問便會發(fā)生嚴(yán)重的錯誤。

我們來修正上面的例子:

fn main() {

? ? let a = 100_i32;

? ? {

? ? ? ? let x = &a;

? ? ? ? println!("{}", x);

? ? }? // x 作用域結(jié)束

}

這里我們僅僅把?println!?放到了中間的?{}, 這樣就可以在?x的生命周期內(nèi)正常的訪問?x?,此時的Lifetime圖示如下:

? ? ? ? ? ? {? ? a? ? {? ? x? ? *? ? }? ? }

所有者 a:? ? ? ? |________________________|

借用者 x:? ? ? ? ? ? ? ? ? |_________|? ? ? x = &a

? 訪問 x:? ? ? ? ? ? ? ? ? ? ? ? |? ? ? ? ? ? OK:訪問 x

隱式Lifetime

我們經(jīng)常會遇到參數(shù)或者返回值為引用類型的函數(shù):

fn foo(x: &str) -> &str {

? ? x

}

上面函數(shù)在實際應(yīng)用中并沒有太多用處,foo?函數(shù)僅僅接受一個?&str?類型的參數(shù)(x為對某個string類型資源Something的借用),并返回對資源Something的一個新的借用。

實際上,上面函數(shù)包含該了隱性的生命周期命名,這是由編譯器自動推導(dǎo)的,相當(dāng)于:

fn foo<'a>(x: &'a str) -> &'a str {

? ? x

}

在這里,約束返回值的Lifetime必須大于或等于參數(shù)x的Lifetime。下面函數(shù)寫法也是合法的:

fn foo<'a>(x: &'a str) -> &'a str {

? ? "hello, world!"

}

為什么呢?這是因為字符串"hello, world!"的類型是&'static str,我們知道static類型的Lifetime是整個程序的運(yùn)行周期,所以她比任意傳入的參數(shù)的Lifetime'a都要長,即'static >= 'a滿足。

在上例中Rust可以自動推導(dǎo)Lifetime,所以并不需要程序員顯式指定Lifetime?'a?。

'a是什么呢?它是Lifetime的標(biāo)識符,這里的a也可以用b、c、d、e、...,甚至可以用this_is_a_long_name等,當(dāng)然實際編程中并不建議用這種冗長的標(biāo)識符,這樣會嚴(yán)重降低程序的可讀性。foo后面的<'a>為Lifetime的聲明,可以聲明多個,如<'a, 'b>等等。

另外,除非編譯器無法自動推導(dǎo)出Lifetime,否則不建議顯式指定Lifetime標(biāo)識符,會降低程序的可讀性。

顯式Lifetime

當(dāng)輸入?yún)?shù)為多個借用/引用時會發(fā)生什么呢?

fn foo(x: &str, y: &str) -> &str {

? ? if true {

? ? ? ? x

? ? } else {

? ? ? ? y

? ? }

}

這時候再編譯,就沒那么幸運(yùn)了:

error: missing lifetime specifier [E0106]

fn foo(x: &str, y: &str) -> &str {

? ? ? ? ? ? ? ? ? ? ? ? ? ? ^~~~

編譯器告訴我們,需要我們顯式指定Lifetime標(biāo)識符,因為這個時候,編譯器無法推導(dǎo)出返回值的Lifetime應(yīng)該是比?x長,還是比y長。雖然我們在函數(shù)中中用了?if true?確認(rèn)一定可以返回x,但是要知道,編譯器是在編譯時候檢查,而不是運(yùn)行時,所以編譯期間會同時檢查所有的輸入?yún)?shù)和返回值。

修復(fù)后的代碼如下:

fn foo<'a>(x: &'a str, y: &'a str) -> &'a str {

? ? if true {

? ? ? ? x

? ? } else {

? ? ? ? y

? ? }

}

Lifetime推導(dǎo)

要推導(dǎo)Lifetime是否合法,先明確兩點(diǎn):

輸出值(也稱為返回值)依賴哪些輸入值

輸入值的Lifetime大于或等于輸出值的Lifetime (準(zhǔn)確來說:子集,而不是大于或等于)

Lifetime推導(dǎo)公式:?當(dāng)輸出值R依賴輸入值X Y Z ...,當(dāng)且僅當(dāng)輸出值的Lifetime為所有輸入值的Lifetime交集的子集時,生命周期合法。

? ? Lifetime(R) ? ( Lifetime(X) ∩ Lifetime(Y) ∩ Lifetime(Z) ∩ Lifetime(...) )

對于例子1:

fn foo<'a>(x: &'a str, y: &'a str) -> &'a str {

? ? if true {

? ? ? ? x

? ? } else {

? ? ? ? y

? ? }

}

因為返回值同時依賴輸入?yún)?shù)x和y,所以

? ? Lifetime(返回值) ? ( Lifetime(x) ∩ Lifetime(y) )

? ? 即:

? ? 'a ? ('a ∩ 'a)? // 成立

定義多個Lifetime標(biāo)識符

那我們繼續(xù)看個更復(fù)雜的例子,定義多個Lifetime標(biāo)識符:

fn foo<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {

? ? if true {

? ? ? ? x

? ? } else {

? ? ? ? y

? ? }

}

先看下編譯,又報錯了:

<anon>:5:3: 5:4 error: cannot infer an appropriate lifetime for automatic coercion due to conflicting requirements [E0495]

<anon>:5? ? ? ? y

? ? ? ? ? ? ? ? ^

<anon>:1:1: 7:2 help: consider using an explicit lifetime parameter as shown: fn foo<'a>(x: &'a str, y: &'a str) -> &'a str

<anon>:1 fn bar<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {

<anon>:2? ? if true {

<anon>:3? ? ? ? x

<anon>:4? ? } else {

<anon>:5? ? ? ? y

<anon>:6? ? }

編譯器說自己無法正確地推導(dǎo)返回值的Lifetime,讀者可能會疑問,“我們不是已經(jīng)指定返回值的Lifetime為'a了嗎?"。

這兒我們同樣可以通過生命周期推導(dǎo)公式推導(dǎo):

因為返回值同時依賴x和y,所以

? ? Lifetime(返回值) ? ( Lifetime(x) ∩ Lifetime(y) )

? ? 即:

? ? 'a ? ('a ∩ 'b)? //不成立

很顯然,上面我們根本沒法保證成立。

所以,這種情況下,我們可以顯式地告訴編譯器'b比'a長('a是'b的子集),只需要在定義Lifetime的時候, 在'b的后面加上: 'a, 意思是'b比'a長,'a是'b的子集:

fn foo<'a, 'b: 'a>(x: &'a str, y: &'b str) -> &'a str {

? ? if true {

? ? ? ? x

? ? } else {

? ? ? ? y

? ? }

}

這里我們根據(jù)公式繼續(xù)推導(dǎo):

? ? 條件:Lifetime(x) ? Lifetime(y)

? ? 推導(dǎo):Lifetime(返回值) ? ( Lifetime(x) ∩ Lifetime(y) )

? ? 即:

? ? 條件: 'a ? 'b

? ? 推導(dǎo):'a ? ('a ∩ 'b) // 成立

上面是成立的,所以可以編譯通過。

推導(dǎo)總結(jié)

通過上面的學(xué)習(xí)相信大家可以很輕松完成Lifetime的推導(dǎo),總之,記住兩點(diǎn):

輸出值依賴哪些輸入值。

推導(dǎo)公式。

Lifetime in struct

上面我們更多討論了函數(shù)中Lifetime的應(yīng)用,在struct中Lifetime同樣重要。

我們來定義一個Person結(jié)構(gòu)體:

struct Person {

? ? age: &u8,

}

編譯時我們會得到一個error:

<anon>:2:8: 2:12 error: missing lifetime specifier [E0106]

<anon>:2? ? age: &str,

之所以會報錯,這是因為Rust要確保Person的Lifetime不會比它的age借用長,不然會出現(xiàn)Dangling Pointer的嚴(yán)重內(nèi)存問題。所以我們需要為age借用聲明Lifetime:

struct Person<'a> {

? ? age: &'a u8,

}

不需要對Person后面的<'a>感到疑惑,這里的'a并不是指Person這個struct的Lifetime,僅僅是一個泛型參數(shù)而已,struct可以有多個Lifetime參數(shù)用來約束不同的field,實際的Lifetime應(yīng)該是所有fieldLifetime交集的子集。例如:

fn main() {

? ? let x = 20_u8;

? ? let stormgbs = Person {

? ? ? ? ? ? ? ? ? ? ? ? age: &x,

? ? ? ? ? ? ? ? ? ? };

}

這里,生命周期/Scope的示意圖如下:

? ? ? ? ? ? ? ? ? {? x? ? stormgbs? ? ? *? ? }

所有者 x:? ? ? ? ? ? ? |________________________|

所有者 stormgbs:? ? ? ? ? ? ? ? |_______________|? 'a

借用者 stormgbs.age:? ? ? ? ? ? |_______________|? stormgbs.age = &x

既然<'a>作為Person的泛型參數(shù),所以在為Person實現(xiàn)方法時也需要加上<'a>,不然:

impl Person {

? ? fn print_age(&self) {

? ? ? ? println!("Person.age = {}", self.age);

? ? }

}

報錯:

<anon>:5:6: 5:12 error: wrong number of lifetime parameters: expected 1, found 0 [E0107]

<anon>:5 impl Person {

? ? ? ? ? ? ? ^~~~~~

正確的做法是

impl<'a> Person<'a> {

? ? fn print_age(&self) {

? ? ? ? println!("Person.age = {}", self.age);

? ? }

}

這樣加上<'a>后就可以了。讀者可能會疑問,為什么print_age中不需要加上'a?這是個好問題。因為print_age的輸出參數(shù)為(),也就是可以不依賴任何輸入?yún)?shù), 所以編譯器此時可以不必關(guān)心和推導(dǎo)Lifetime。即使是fn print_age(&self, other_age: &i32) {...}也可以編譯通過。

如果Person的方法存在輸出值(借用)呢?

impl<'a> Person<'a> {

? ? fn get_age(&self) -> &u8 {

? ? ? ? self.age

? ? }

}

get_age方法的輸出值依賴一個輸入值&self,這種情況下,Rust編譯器可以自動推導(dǎo)為:

impl<'a> Person<'a> {

? ? fn get_age(&'a self) -> &'a u8 {

? ? ? ? self.age

? ? }

}

如果輸出值(借用)依賴了多個輸入值呢?

impl<'a, 'b> Person<'a> {

? ? fn get_max_age(&'a self, p: &'a Person) -> &'a u8 {

? ? ? ? if self.age > p.age {

? ? ? ? ? ? self.age

? ? ? ? } else {

? ? ? ? ? ? p.age

? ? ? ? }

? ? }

}

類似之前的Lifetime推導(dǎo)章節(jié),當(dāng)返回值(借用)依賴多個輸入值時,需顯示聲明Lifetime。和函數(shù)Lifetime同理。

其他

無論在函數(shù)還是在struct中,甚至在enum中,Lifetime理論知識都是一樣的。希望大家可以慢慢體會和吸收,做到舉一反三。

總結(jié)

Rust正是通過所有權(quán)、借用以及生命周期,以高效、安全的方式近乎完美地管理了內(nèi)存。沒有手動管理內(nèi)存的負(fù)載和安全性,也沒有GC造成的程序暫停問題。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容