ch10-03-lifetime-syntax.md
commit 1b8746013079f2e2ce1c8e85f633d9769778ea7f
當在第四章討論 “引用和借用” 部分時,我們遺漏了一個重要的細節(jié):Rust 中的每一個引用都有其 生命周期(lifetime),也就是引用保持有效的作用域。大部分時候生命周期是隱含并可以推斷的,正如大部分時候類型也是可以推斷的一樣。類似于當因為有多種可能類型的時候必須注明類型,也會出現(xiàn)引用的生命周期以一些不同方式相關(guān)聯(lián)的情況,所以 Rust 需要我們使用泛型生命周期參數(shù)來注明他們的關(guān)系,這樣就能確保運行時實際使用的引用絕對是有效的。
生命周期注解甚至不是一個大部分語言都有的概念,所以這可能感覺起來有些陌生。雖然本章不可能涉及到它全部的內(nèi)容,我們會講到一些通常你可能會遇到的生命周期語法以便你熟悉這個概念。
生命周期的主要目標是避免懸垂引用,后者會導致程序引用了非預期引用的數(shù)據(jù)。考慮一下示例 10-17 中的程序,它有一個外部作用域和一個內(nèi)部作用域。
{
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r);
}
示例 10-17:嘗試使用離開作用域的值的引用
注意:示例 10-17、10-18 和 10-24 中聲明了沒有初始值的變量,所以這些變量存在于外部作用域。這乍看之下好像和 Rust 不允許存在空值相沖突。然而如果嘗試在給它一個值之前使用這個變量,會出現(xiàn)一個編譯時錯誤,這就說明了 Rust 確實不允許空值。
外部作用域聲明了一個沒有初值的變量 r
,而內(nèi)部作用域聲明了一個初值為 5 的變量x
。在內(nèi)部作用域中,我們嘗試將 r
的值設置為一個 x
的引用。接著在內(nèi)部作用域結(jié)束后,嘗試打印出 r
的值。這段代碼不能編譯因為 r
引用的值在嘗試使用之前就離開了作用域。如下是錯誤信息:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `x` does not live long enough
--> src/main.rs:7:17
|
7 | r = &x;
| ^^ borrowed value does not live long enough
8 | }
| - `x` dropped here while still borrowed
9 |
10 | println!("r: {}", r);
| - borrow later used here
For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` due to previous error
變量 x
并沒有 “存在的足夠久”。其原因是 x
在到達第 7 行內(nèi)部作用域結(jié)束時就離開了作用域。不過 r
在外部作用域仍是有效的;作用域越大我們就說它 “存在的越久”。如果 Rust 允許這段代碼工作,r
將會引用在 x
離開作用域時被釋放的內(nèi)存,這時嘗試對 r
做任何操作都不能正常工作。那么 Rust 是如何決定這段代碼是不被允許的呢?這得益于借用檢查器。
Rust 編譯器有一個 借用檢查器(borrow checker),它比較作用域來確保所有的借用都是有效的。示例 10-18 展示了與示例 10-17 相同的例子不過帶有變量生命周期的注釋:
{
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+
示例 10-18:r
和 x
的生命周期注解,分別叫做 'a
和 'b
這里將 r
的生命周期標記為 'a
并將 x
的生命周期標記為 'b
。如你所見,內(nèi)部的 'b
塊要比外部的生命周期 'a
小得多。在編譯時,Rust 比較這兩個生命周期的大小,并發(fā)現(xiàn) r
擁有生命周期 'a
,不過它引用了一個擁有生命周期 'b
的對象。程序被拒絕編譯,因為生命周期 'b
比生命周期 'a
要?。罕灰玫膶ο蟊人囊谜叽嬖诘臅r間更短。
讓我們看看示例 10-19 中這個并沒有產(chǎn)生懸垂引用且可以正確編譯的例子:
{
let x = 5; // ----------+-- 'b
// |
let r = &x; // --+-- 'a |
// | |
println!("r: {}", r); // | |
// --+ |
} // ----------+
示例 10-19:一個有效的引用,因為數(shù)據(jù)比引用有著更長的生命周期
這里 x
擁有生命周期 'b
,比 'a
要大。這就意味著 r
可以引用 x
:Rust 知道 r
中的引用在 x
有效的時候也總是有效的。
現(xiàn)在我們已經(jīng)在一個具體的例子中展示了引用的生命周期位于何處,并討論了 Rust 如何分析生命周期來保證引用總是有效的,接下來讓我們聊聊在函數(shù)的上下文中參數(shù)和返回值的泛型生命周期。
讓我們來編寫一個返回兩個字符串 slice 中較長者的函數(shù)。這個函數(shù)獲取兩個字符串 slice 并返回一個字符串 slice。一旦我們實現(xiàn)了 longest
函數(shù),示例 10-20 中的代碼應該會打印出 The longest string is abcd
:
文件名: src/main.rs
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}
示例 10-20:main
函數(shù)調(diào)用 longest
函數(shù)來尋找兩個字符串 slice 中較長的一個
注意這個函數(shù)獲取作為引用的字符串 slice,因為我們不希望 longest
函數(shù)獲取參數(shù)的所有權(quán)。參考之前第四章中的 “字符串 slice 作為參數(shù)” 部分中更多關(guān)于為什么示例 10-20 的參數(shù)正符合我們期望的討論。
如果嘗試像示例 10-21 中那樣實現(xiàn) longest
函數(shù),它并不能編譯:
文件名: src/main.rs
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
示例 10-21:一個 longest
函數(shù)的實現(xiàn),它返回兩個字符串 slice 中較長者,現(xiàn)在還不能編譯
相應地會出現(xiàn)如下有關(guān)生命周期的錯誤:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
--> src/main.rs:9:33
|
9 | fn longest(x: &str, y: &str) -> &str {
| ---- ---- ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
|
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
| ++++ ++ ++ ++
For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10` due to previous error
提示文本揭示了返回值需要一個泛型生命周期參數(shù),因為 Rust 并不知道將要返回的引用是指向 x
或 y
。事實上我們也不知道,因為函數(shù)體中 if
塊返回一個 x
的引用而 else
塊返回一個 y
的引用!
當我們定義這個函數(shù)的時候,并不知道傳遞給函數(shù)的具體值,所以也不知道到底是 if
還是 else
會被執(zhí)行。我們也不知道傳入的引用的具體生命周期,所以也就不能像示例 10-18 和 10-19 那樣通過觀察作用域來確定返回的引用是否總是有效。借用檢查器自身同樣也無法確定,因為它不知道 x
和 y
的生命周期是如何與返回值的生命周期相關(guān)聯(lián)的。為了修復這個錯誤,我們將增加泛型生命周期參數(shù)來定義引用間的關(guān)系以便借用檢查器可以進行分析。
生命周期注解并不改變?nèi)魏我玫纳芷诘拈L短。與當函數(shù)簽名中指定了泛型類型參數(shù)后就可以接受任何類型一樣,當指定了泛型生命周期后函數(shù)也能接受任何生命周期的引用。生命周期注解描述了多個引用生命周期相互的關(guān)系,而不影響其生命周期。
生命周期注解有著一個不太常見的語法:生命周期參數(shù)名稱必須以撇號('
)開頭,其名稱通常全是小寫,類似于泛型其名稱非常短。'a
是大多數(shù)人默認使用的名稱。生命周期參數(shù)注解位于引用的 &
之后,并有一個空格來將引用類型與生命周期注解分隔開。
這里有一些例子:我們有一個沒有生命周期參數(shù)的 i32
的引用,一個有叫做 'a
的生命周期參數(shù)的 i32
的引用,和一個生命周期也是 'a
的 i32
的可變引用:
&i32 // 引用
&'a i32 // 帶有顯式生命周期的引用
&'a mut i32 // 帶有顯式生命周期的可變引用
單個的生命周期注解本身沒有多少意義,因為生命周期注解告訴 Rust 多個引用的泛型生命周期參數(shù)如何相互聯(lián)系的。例如如果函數(shù)有一個生命周期 'a
的 i32
的引用的參數(shù) first
。還有另一個同樣是生命周期 'a
的 i32
的引用的參數(shù) second
。這兩個生命周期注解意味著引用 first
和 second
必須與這泛型生命周期存在得一樣久。
現(xiàn)在來看看 longest
函數(shù)的上下文中的生命周期。就像泛型類型參數(shù),泛型生命周期參數(shù)需要聲明在函數(shù)名和參數(shù)列表間的尖括號中。 在這個簽名中我們想要表達的限制是所有(兩個)參數(shù)和返回的引用的生命周期是相關(guān)的,也就是這兩個參數(shù)和返回的引用存活的一樣久。就像示例 10-22 中在每個引用中都加上了 'a
那樣:
文件名: src/main.rs
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
示例 10-22:longest
函數(shù)定義指定了簽名中所有的引用必須有相同的生命周期 'a
這段代碼能夠編譯并會產(chǎn)生我們希望得到的示例 10-20 中的 main
函數(shù)的結(jié)果。
現(xiàn)在函數(shù)簽名表明對于某些生命周期 'a
,函數(shù)會獲取兩個參數(shù),他們都是與生命周期 'a
存在的一樣長的字符串 slice。函數(shù)會返回一個同樣也與生命周期 'a
存在的一樣長的字符串 slice。它的實際含義是 longest
函數(shù)返回的引用的生命周期與傳入該函數(shù)的引用的生命周期的較小者一致。這些關(guān)系就是我們希望 Rust 分析代碼時所使用的。
記住通過在函數(shù)簽名中指定生命周期參數(shù)時,我們并沒有改變?nèi)魏蝹魅胫祷蚍祷刂档纳芷?,而是指出任何不滿足這個約束條件的值都將被借用檢查器拒絕。注意 longest
函數(shù)并不需要知道 x
和 y
具體會存在多久,而只需要知道有某個可以被 'a
替代的作用域?qū)M足這個簽名。
當在函數(shù)中使用生命周期注解時,這些注解出現(xiàn)在函數(shù)簽名中,而不存在于函數(shù)體中的任何代碼中。生命周期注解成為了函數(shù)約定的一部分,非常像簽名中的類型。讓函數(shù)簽名包含生命周期約定意味著 Rust 編譯器的工作變得更簡單了。如果函數(shù)注解有誤或者調(diào)用方法不對,編譯器錯誤可以更準確地指出代碼和限制的部分。如果不這么做的話,Rust 編譯會對我們期望的生命周期關(guān)系做更多的推斷,這樣編譯器可能只能指出離出問題地方很多步之外的代碼。
當具體的引用被傳遞給 longest
時,被 'a
所替代的具體生命周期是 x
的作用域與 y
的作用域相重疊的那一部分。換一種說法就是泛型生命周期 'a
的具體生命周期等同于 x
和 y
的生命周期中較小的那一個。因為我們用相同的生命周期參數(shù) 'a
標注了返回的引用值,所以返回的引用值就能保證在 x
和 y
中較短的那個生命周期結(jié)束之前保持有效。
讓我們看看如何通過傳遞擁有不同具體生命周期的引用來限制 longest
函數(shù)的使用。示例 10-23 是一個很直觀的例子。
文件名: src/main.rs
fn main() {
let string1 = String::from("long string is long");
{
let string2 = String::from("xyz");
let result = longest(string1.as_str(), string2.as_str());
println!("The longest string is {}", result);
}
}
示例 10-23:通過擁有不同的具體生命周期的 String
值調(diào)用 longest
函數(shù)
在這個例子中,string1
直到外部作用域結(jié)束都是有效的,string2
則在內(nèi)部作用域中是有效的,而 result
則引用了一些直到內(nèi)部作用域結(jié)束都是有效的值。借用檢查器認可這些代碼;它能夠編譯和運行,并打印出 The longest string is long string is long
。
接下來,讓我們嘗試另外一個例子,該例子揭示了 result
的引用的生命周期必須是兩個參數(shù)中較短的那個。以下代碼將 result
變量的聲明移動出內(nèi)部作用域,但是將 result
和 string2
變量的賦值語句一同留在內(nèi)部作用域中。接著,使用了變量 result
的 println!
也被移動到內(nèi)部作用域之外。注意示例 10-24 中的代碼不能通過編譯:
文件名: src/main.rs
fn main() {
let string1 = String::from("long string is long");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("The longest string is {}", result);
}
示例 10-24:嘗試在 string2
離開作用域之后使用 result
如果嘗試編譯會出現(xiàn)如下錯誤:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `string2` does not live long enough
--> src/main.rs:6:44
|
6 | result = longest(string1.as_str(), string2.as_str());
| ^^^^^^^^^^^^^^^^ borrowed value does not live long enough
7 | }
| - `string2` dropped here while still borrowed
8 | println!("The longest string is {}", result);
| ------ borrow later used here
For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` due to previous error
錯誤表明為了保證 println!
中的 result
是有效的,string2
需要直到外部作用域結(jié)束都是有效的。Rust 知道這些是因為(longest
)函數(shù)的參數(shù)和返回值都使用了相同的生命周期參數(shù) 'a
。
如果從人的角度讀上述代碼,我們可能會覺得這個代碼是正確的。 string1
更長,因此 result
會包含指向 string1
的引用。因為 string1
尚未離開作用域,對于 println!
來說 string1
的引用仍然是有效的。然而,我們通過生命周期參數(shù)告訴 Rust 的是: longest
函數(shù)返回的引用的生命周期應該與傳入?yún)?shù)的生命周期中較短那個保持一致。因此,借用檢查器不允許示例 10-24 中的代碼,因為它可能會存在無效的引用。
請嘗試更多采用不同的值和不同生命周期的引用作為 longest
函數(shù)的參數(shù)和返回值的實驗。并在開始編譯前猜想你的實驗能否通過借用檢查器,接著編譯一下看看你的理解是否正確!
指定生命周期參數(shù)的正確方式依賴函數(shù)實現(xiàn)的具體功能。例如,如果將 longest
函數(shù)的實現(xiàn)修改為總是返回第一個參數(shù)而不是最長的字符串 slice,就不需要為參數(shù) y
指定一個生命周期。如下代碼將能夠編譯:
文件名: src/main.rs
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
x
}
在這個例子中,我們?yōu)閰?shù) x
和返回值指定了生命周期參數(shù) 'a
,不過沒有為參數(shù) y
指定,因為 y
的生命周期與參數(shù) x
和返回值的生命周期沒有任何關(guān)系。
當從函數(shù)返回一個引用,返回值的生命周期參數(shù)需要與一個參數(shù)的生命周期參數(shù)相匹配。如果返回的引用 沒有 指向任何一個參數(shù),那么唯一的可能就是它指向一個函數(shù)內(nèi)部創(chuàng)建的值,它將會是一個懸垂引用,因為它將會在函數(shù)結(jié)束時離開作用域。嘗試考慮這個并不能編譯的 longest
函數(shù)實現(xiàn):
文件名: src/main.rs
fn longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("really long string");
result.as_str()
}
即便我們?yōu)榉祷刂抵付松芷趨?shù) 'a
,這個實現(xiàn)卻編譯失敗了,因為返回值的生命周期與參數(shù)完全沒有關(guān)聯(lián)。這里是會出現(xiàn)的錯誤信息:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0515]: cannot return reference to local variable `result`
--> src/main.rs:11:5
|
11 | result.as_str()
| ^^^^^^^^^^^^^^^ returns a reference to data owned by the current function
For more information about this error, try `rustc --explain E0515`.
error: could not compile `chapter10` due to previous error
出現(xiàn)的問題是 result
在 longest
函數(shù)的結(jié)尾將離開作用域并被清理,而我們嘗試從函數(shù)返回一個 result
的引用。無法指定生命周期參數(shù)來改變懸垂引用,而且 Rust 也不允許我們創(chuàng)建一個懸垂引用。在這種情況,最好的解決方案是返回一個有所有權(quán)的數(shù)據(jù)類型而不是一個引用,這樣函數(shù)調(diào)用者就需要負責清理這個值了。
綜上,生命周期語法是用于將函數(shù)的多個參數(shù)與其返回值的生命周期進行關(guān)聯(lián)的。一旦他們形成了某種關(guān)聯(lián),Rust 就有了足夠的信息來允許內(nèi)存安全的操作并阻止會產(chǎn)生懸垂指針亦或是違反內(nèi)存安全的行為。
目前為止,我們只定義過有所有權(quán)類型的結(jié)構(gòu)體。接下來,我們將定義包含引用的結(jié)構(gòu)體,不過這需要為結(jié)構(gòu)體定義中的每一個引用添加生命周期注解。示例 10-25 中有一個存放了一個字符串 slice 的結(jié)構(gòu)體 ImportantExcerpt
:
文件名: src/main.rs
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
let i = ImportantExcerpt {
part: first_sentence,
};
}
示例 10-25:一個存放引用的結(jié)構(gòu)體,所以其定義需要生命周期注解
這個結(jié)構(gòu)體有一個字段,part
,它存放了一個字符串 slice,這是一個引用。類似于泛型參數(shù)類型,必須在結(jié)構(gòu)體名稱后面的尖括號中聲明泛型生命周期參數(shù),以便在結(jié)構(gòu)體定義中使用生命周期參數(shù)。這個注解意味著 ImportantExcerpt
的實例不能比其 part
字段中的引用存在的更久。
這里的 main
函數(shù)創(chuàng)建了一個 ImportantExcerpt
的實例,它存放了變量 novel
所擁有的 String
的第一個句子的引用。novel
的數(shù)據(jù)在 ImportantExcerpt
實例創(chuàng)建之前就存在。另外,直到 ImportantExcerpt
離開作用域之后 novel
都不會離開作用域,所以 ImportantExcerpt
實例中的引用是有效的。
現(xiàn)在我們已經(jīng)知道了每一個引用都有一個生命周期,而且我們需要為那些使用了引用的函數(shù)或結(jié)構(gòu)體指定生命周期。然而,第四章的示例 4-9 中有一個函數(shù),如示例 10-26 所示,它沒有生命周期注解卻能編譯成功:
文件名: src/lib.rs
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
示例 10-26:示例 4-9 定義了一個沒有使用生命周期注解的函數(shù),即便其參數(shù)和返回值都是引用
這個函數(shù)沒有生命周期注解卻能編譯是由于一些歷史原因:在早期版本(pre-1.0)的 Rust 中,這的確是不能編譯的。每一個引用都必須有明確的生命周期。那時的函數(shù)簽名將會寫成這樣:
fn first_word<'a>(s: &'a str) -> &'a str {
在編寫了很多 Rust 代碼后,Rust 團隊發(fā)現(xiàn)在特定情況下 Rust 程序員們總是重復地編寫一模一樣的生命周期注解。這些場景是可預測的并且遵循幾個明確的模式。接著 Rust 團隊就把這些模式編碼進了 Rust 編譯器中,如此借用檢查器在這些情況下就能推斷出生命周期而不再強制程序員顯式的增加注解。
這里我們提到一些 Rust 的歷史是因為更多的明確的模式被合并和添加到編譯器中是完全可能的。未來只會需要更少的生命周期注解。
被編碼進 Rust 引用分析的模式被稱為 生命周期省略規(guī)則(lifetime elision rules)。這并不是需要程序員遵守的規(guī)則;這些規(guī)則是一系列特定的場景,此時編譯器會考慮,如果代碼符合這些場景,就無需明確指定生命周期。
省略規(guī)則并不提供完整的推斷:如果 Rust 在明確遵守這些規(guī)則的前提下變量的生命周期仍然是模棱兩可的話,它不會猜測剩余引用的生命周期應該是什么。在這種情況,編譯器會給出一個錯誤,這可以通過增加對應引用之間相聯(lián)系的生命周期注解來解決。
函數(shù)或方法的參數(shù)的生命周期被稱為 輸入生命周期(input lifetimes),而返回值的生命周期被稱為 輸出生命周期(output lifetimes)。
編譯器采用三條規(guī)則來判斷引用何時不需要明確的注解。第一條規(guī)則適用于輸入生命周期,后兩條規(guī)則適用于輸出生命周期。如果編譯器檢查完這三條規(guī)則后仍然存在沒有計算出生命周期的引用,編譯器將會停止并生成錯誤。這些規(guī)則適用于 fn
定義,以及 impl
塊。
第一條規(guī)則是每一個是引用的參數(shù)都有它自己的生命周期參數(shù)。換句話說就是,有一個引用參數(shù)的函數(shù)有一個生命周期參數(shù):fn foo<'a>(x: &'a i32)
,有兩個引用參數(shù)的函數(shù)有兩個不同的生命周期參數(shù),fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
,依此類推。
第二條規(guī)則是如果只有一個輸入生命周期參數(shù),那么它被賦予所有輸出生命周期參數(shù):fn foo<'a>(x: &'a i32) -> &'a i32
。
第三條規(guī)則是如果方法有多個輸入生命周期參數(shù)并且其中一個參數(shù)是 &self
或 &mut self
,說明是個對象的方法(method)(譯者注: 這里涉及rust的面向?qū)ο髤⒁?7章),那么所有輸出生命周期參數(shù)被賦予 self
的生命周期。第三條規(guī)則使得方法更容易讀寫,因為只需更少的符號。
假設我們自己就是編譯器。并應用這些規(guī)則來計算示例 10-26 中 first_word
函數(shù)簽名中的引用的生命周期。開始時簽名中的引用并沒有關(guān)聯(lián)任何生命周期:
fn first_word(s: &str) -> &str {
接著編譯器應用第一條規(guī)則,也就是每個引用參數(shù)都有其自己的生命周期。我們像往常一樣稱之為 'a
,所以現(xiàn)在簽名看起來像這樣:
fn first_word<'a>(s: &'a str) -> &str {
對于第二條規(guī)則,因為這里正好只有一個輸入生命周期參數(shù)所以是適用的。第二條規(guī)則表明輸入?yún)?shù)的生命周期將被賦予輸出生命周期參數(shù),所以現(xiàn)在簽名看起來像這樣:
fn first_word<'a>(s: &'a str) -> &'a str {
現(xiàn)在這個函數(shù)簽名中的所有引用都有了生命周期,如此編譯器可以繼續(xù)它的分析而無須程序員標記這個函數(shù)簽名中的生命周期。
讓我們再看看另一個例子,這次我們從示例 10-21 中沒有生命周期參數(shù)的 longest
函數(shù)開始:
fn longest(x: &str, y: &str) -> &str {
再次假設我們自己就是編譯器并應用第一條規(guī)則:每個引用參數(shù)都有其自己的生命周期。這次有兩個參數(shù),所以就有兩個(不同的)生命周期:
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
再來應用第二條規(guī)則,因為函數(shù)存在多個輸入生命周期,它并不適用于這種情況。再來看第三條規(guī)則,它同樣也不適用,這是因為沒有 self
參數(shù)。應用了三個規(guī)則之后編譯器還沒有計算出返回值類型的生命周期。這就是為什么在編譯示例 10-21 的代碼時會出現(xiàn)錯誤的原因:編譯器使用所有已知的生命周期省略規(guī)則,仍不能計算出簽名中所有引用的生命周期。
因為第三條規(guī)則真正能夠適用的就只有方法簽名,現(xiàn)在就讓我們看看那種情況中的生命周期,并看看為什么這條規(guī)則意味著我們經(jīng)常不需要在方法簽名中標注生命周期。
當為帶有生命周期的結(jié)構(gòu)體實現(xiàn)方法時,其語法依然類似示例 10-11 中展示的泛型類型參數(shù)的語法。聲明和使用生命周期參數(shù)的位置依賴于生命周期參數(shù)是否同結(jié)構(gòu)體字段或方法參數(shù)和返回值相關(guān)。
(實現(xiàn)方法時)結(jié)構(gòu)體字段的生命周期必須總是在 impl
關(guān)鍵字之后聲明并在結(jié)構(gòu)體名稱之后被使用,因為這些生命周期是結(jié)構(gòu)體類型的一部分。
impl
塊里的方法簽名中,引用可能與結(jié)構(gòu)體字段中的引用相關(guān)聯(lián),也可能是獨立的。另外,生命周期省略規(guī)則也經(jīng)常讓我們無需在方法簽名中使用生命周期注解。讓我們看看一些使用示例 10-25 中定義的結(jié)構(gòu)體 ImportantExcerpt
的例子。
首先,這里有一個方法 level
。其唯一的參數(shù)是 self
的引用,而且返回值只是一個 i32
,并不引用任何值:
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}
impl
之后和類型名稱之后的生命周期參數(shù)是必要的,不過因為第一條生命周期規(guī)則我們并不必須標注 self
引用的生命周期。
這里是一個適用于第三條生命周期省略規(guī)則的例子:
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
這里有兩個輸入生命周期,所以 Rust 應用第一條生命周期省略規(guī)則并給予 &self
和 announcement
他們各自的生命周期。接著,因為其中一個參數(shù)是 &self
,返回值類型被賦予了 &self
的生命周期,這樣所有的生命周期都被計算出來了。
這里有一種特殊的生命周期值得討論:'static
,其生命周期能夠存活于整個程序期間。所有的字符串字面值都擁有 'static
生命周期,我們也可以選擇像下面這樣標注出來:
let s: &'static str = "I have a static lifetime.";
這個字符串的文本被直接儲存在程序的二進制文件中而這個文件總是可用的。因此所有的字符串字面值都是 'static
的。
你可能在錯誤信息的幫助文本中見過使用 'static
生命周期的建議,不過將引用指定為 'static
之前,思考一下這個引用是否真的在整個程序的生命周期里都有效。你也許要考慮是否希望它存在得這么久,即使這是可能的。大部分情況,代碼中的問題是嘗試創(chuàng)建一個懸垂引用或者可用的生命周期不匹配,請解決這些問題而不是指定一個 'static
的生命周期。
讓我們簡要的看一下在同一函數(shù)中指定泛型類型參數(shù)、trait bounds 和生命周期的語法!
use std::fmt::Display;
fn longest_with_an_announcement<'a, T>(
x: &'a str,
y: &'a str,
ann: T,
) -> &'a str
where
T: Display,
{
println!("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}
這個是示例 10-22 中那個返回兩個字符串 slice 中較長者的 longest
函數(shù),不過帶有一個額外的參數(shù) ann
。ann
的類型是泛型 T
,它可以被放入任何實現(xiàn)了 where
從句中指定的 Display
trait 的類型。這個額外的參數(shù)會使用 {}
打印,這也就是為什么 Display
trait bound 是必須的。因為生命周期也是泛型,所以生命周期參數(shù) 'a
和泛型類型參數(shù) T
都位于函數(shù)名后的同一尖括號列表中。
這一章介紹了很多的內(nèi)容!現(xiàn)在你知道了泛型類型參數(shù)、trait 和 trait bounds 以及泛型生命周期類型,你已經(jīng)準備好編寫既不重復又能適用于多種場景的代碼了。泛型類型參數(shù)意味著代碼可以適用于不同的類型。trait 和 trait bounds 保證了即使類型是泛型的,這些類型也會擁有所需要的行為。由生命周期注解所指定的引用生命周期之間的關(guān)系保證了這些靈活多變的代碼不會出現(xiàn)懸垂引用。而所有的這一切發(fā)生在編譯時所以不會影響運行時效率!
你可能不會相信,這個話題還有更多需要學習的內(nèi)容:第十七章會討論 trait 對象,這是另一種使用 trait 的方式。還有更多更復雜的涉及生命周期注解的場景,只有在非常高級的情況下才會需要它們;對于這些內(nèi)容,請閱讀 Rust Reference。不過接下來,讓我們聊聊如何在 Rust 中編寫測試,來確保代碼的所有功能能像我們希望的那樣工作!
更多建議: