下面是一個資源借用的例子:
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
在離開自己的作用域后,無法在作用域之外訪問。
上面的涉及到幾個概念:
a
x
強調(diào)下,無論是資源的所有者還是資源的借用/引用,都存在在一個有效的存活時間或區(qū)間,這個時間區(qū)間稱為生命周期, 也可以直接以Scope作用域去理解。
所以上例子代碼中的生命周期/作用域圖示如下:
{ a { x } * }
所有者 a: |________________________|
借用者 x: |____| x = &a
訪問 x: | 失敗:訪問 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
我們經(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是整個程序的運行周期,所以她比任意傳入的參數(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)識符,會降低程序的可讀性。
當(dāng)輸入?yún)?shù)為多個借用/引用時會發(fā)生什么呢?
fn foo(x: &str, y: &str) -> &str {
if true {
x
} else {
y
}
}
這時候再編譯,就沒那么幸運了:
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)?shù)和返回值。
修復(fù)后的代碼如下:
fn foo<'a>(x: &'a str, y: &'a str) -> &'a str {
if true {
x
} else {
y
}
}
要推導(dǎo)Lifetime是否合法,先明確兩點:
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) // 成立
那我們繼續(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) // 成立
上面是成立的,所以可以編譯通過。
通過上面的學(xué)習(xí)相信大家可以很輕松完成Lifetime的推導(dǎo),總之,記住兩點:
上面我們更多討論了函數(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)該是所有field
Lifetime交集的子集。例如:
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理論知識都是一樣的。希望大家可以慢慢體會和吸收,做到舉一反三。
Rust正是通過所有權(quán)、借用以及生命周期,以高效、安全的方式近乎完美地管理了內(nèi)存。沒有手動管理內(nèi)存的負載和安全性,也沒有GC造成的程序暫停問題。