生命周期( 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造成的程序暫停問題。