透過生命週期驗證引用
我們在第四章的「引用與借用」段落沒談到的是,Rust 中的每個引用都有個生命週期(lifetime),這是決定該引用是否有效的作用域。大多情況下生命週期是隱式且可推導出來得,就像大多情況下型別是可推導出來的。當多種型別都有可能時,我們就得詮釋型別。同樣地,當生命週期的引用能以不同方式關聯的話,我們就得詮釋生命週期。Rust 要求我們用泛型生命週期參數來詮釋引用之間的關係,以確保實際在執行時的引用絕對是有效的。
生命週期的概念與其他程式語言有的工具都大相徑庭,這讓生命週期成為 Rust 最獨特的特色。雖然我們不會在此章涵蓋所有生命週期的內容,但是我們講些你可能遇到生命週期的常見場景,來讓你更加熟悉這個概念。
透過生命週期預防迷途引用
生命週期最主要的目的就是要預防迷途引用(dangling references),其會導致程式引用到其他資料,而非它原本想要的引用。請看一下範例 10-17 的程式,它有一個外部作用域與內部作用域。
fn main() {
{
let r;
{
let x = 5;
r = &x;
}
println!("r: {}", r);
}
}
範例 10-17:嘗試使用其值已經離開作用域的引用
注意:範例 10-17、10-18 與 10-24 宣告變數時都沒有給予初始數值,所以變數名稱可以存在於外部作用域。乍看之下這似乎違反 Rust 不存在空值的原則。但是如果我們嘗試在賦值前使用變數的話,我們就會獲得編譯期錯誤,這證明 Rust 的確不允許空值。
外部作用域宣告了一個沒有初始值的變數 r
,然後內部作用域宣告了一個初始值為 5 的變數 x
。在內部作用域中,我們嘗試將 x
的引用賦值給 r
。然後內部作用域結束後,我們嘗試印出 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
error: aborting due to previous error
For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10`.
To learn more, run the command again with --verbose.
變數 x
「存在的不夠久」。原因是因為當內部作用域在第 7 行結束時,x
會離開作用域。但是 r
卻還在外部作用域中有效,我們會說的「活得比較久」。如果 Rust 允許此程式碼可以執行的話,r
就會引用到 x
離開作用域後被釋放的記憶體位置,然後我們嘗試對 r
做的事情都不會是正確的了。所以 Rust 如何決定此程式碼無效呢?它使用了借用檢查器。
借用檢查器
Rust 編譯器有個借用檢查器(borrow checker)會比較作用域來檢測所有的借用是否有效。範例 10-18 顯示了範例 10-17 的程式碼,但加上了變數生命週期的詮釋。
fn main() {
{
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+
}
範例 10-18:變數 r
與 x
的生命週期詮釋,分別以 'a
和 'b
作為表示
我們在此定義 r
的生命週期詮釋為 'a
而 x
的生命週期為 'b
。如同你所見,內部的 'b
區塊比外部的 'a
生命週期區塊還小。在編譯期間,Rust 會比較兩個生命週期的大小,並看出 r
有生命週期 'a
但它引用的記憶體有生命週期 'b
。程式被回絕的原因是因為 'b
比 'a
還短:被引用的對象比引用者存在的時間還短。
範例 10-19 修正了此程式碼讓它不會存在迷途引用,並能夠正確編譯。
fn main() { { let x = 5; // ----------+-- 'b // | let r = &x; // --+-- 'a | // | | println!("r: {}", r); // | | // --+ | } // ----------+ }
範例 10-19:一個有效引用,因為資料比引用的生命週期還長
x
在此有生命週期 'b
,此時它比 'a
還長。這代表 r
可以引用 x
,因為 Rust 知道 r
的引用在 x
是有效的時候永遠是有效的。
現在你知道引用的生命週期,以及 Rust 如何分析生命週期以確保引用永遠有效了。讓我們來探索函式中參數與回傳值的泛型生命週期。
函式中的泛型生命週期
讓我們寫個回傳兩個字串切片中較長者的函式。此函式會回傳兩個字串切片並回傳一個字串切片。在我們實作 longest
函式後,範例 10-20 的程式碼應該要印出 最長的字串為 abcd
。
檔案名稱:src/main.rs
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("最長的字串為 {}", result);
}
範例 10-20:main
函式呼叫 longest
函式來找出兩個字串切片中較長的
注意我們需要函式接收的字串切片屬於引用,因為我們不希望 longest
函式會取得它參數的所有權。第四章的「字串切片作為參數」段落有提到為何範例 10-20 的參數正是我們所想要使用的參數。
如果我們嘗試實作 longest
函式時,如範例 10-21 所示,它不會編譯過。
檔案名稱:src/main.rs
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("最長的字串為 {}", result);
}
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
範例 10-21:回傳兩個字串中較長者的 longest
函式實作,不過無法編譯成功
我們會看到以下關於生命週期的錯誤:
$ 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 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`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10`.
To learn more, run the command again with --verbose.
提示文字表示回傳型別需要有一個泛型生命週期參數,因為 Rust 無法辨別出回傳的引用指的是 x
還是 y
。事實上,我們也不知道,因為函式本體中的 if
區塊會回傳 x
引用而 else
區塊會回傳 y
引用!
當我們定義函式時,我們不知道傳遞進此函式的實際數值會是什麼,所以我們不知道到底是 if
或 else
的區塊會被執行。我們也不知道傳遞進來的引用實際的生命週期為何,所以我們無法像範例 10-18 和 10-19 那樣觀察作用域,來判定我們回傳的引用會永遠有效。要修正此錯誤,我們要加上泛型生命週期參數來定義引用之間的關係,讓借用檢查器能夠進行分析。
生命週期詮釋語法
生命週期詮釋不會改變引用能存活多久。就像當函式簽名指定了一個泛型型別參數時,函式便能夠接受任意型別一樣。函式可以指定一個泛型生命週期參數,這樣函式就能接受任何生命週期。生命週期詮釋描述了數個引用的生命週期之間互相的關係,而不會影響其生命週期。
生命週期詮釋的語法有一點不一樣:生命週期參數的名稱必須以撇號('
)作為開頭,通常全是小寫且很短,就像泛型型別一樣。大多數的人會使用名稱 'a
。我們將生命週期參數置於引用的 &
之後,並使用空格區隔詮釋與引用的型別。
以下是一些例子:沒有生命週期參數的 i32
引用、有生命週期 'a
的 i32
引用以及有生命週期 'a
的 i32
可變引用。
&i32 // 一個引用
&'a i32 // 一個有顯式生命週期的引用
&'a mut i32 // 一個有顯式生命週期的可變引用
只有自己一個生命週期本身沒有多少意義,因為該詮釋是為了告訴 Rust 數個引用的泛型生命週期參數之間互相的關係。舉例來說,我們有個函式其參數 first
是個 i32
的引用而生命週期為 'a
。此函式還有另一個參數 second
是另一個 i32
的引用而且生命週期也是 'a
。生命週期詮釋意味著引用 first
與 second
必須與此泛型生命週期存活的一樣久。
函式簽名中的生命週期詮釋
現在讓我們研究 longest
函式中的生命週期詮釋吧。如同泛型型別參數,我們需要在函式名稱與參數列表之間的尖括號內宣告泛型生命週期參數。我們想在此簽名表達的是所有參數與回傳值的引用都必須有相同的生命週期。我們將生命週期命名為 'a
然後將它加到每個引用,如範例 10-22 所示。
檔案名稱:src/main.rs
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest(string1.as_str(), string2); println!("最長的字串為 {}", result); } fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
範例 10-22:longest
函式定義指定所有簽名中的引用必須有相同的生命週期 'a
此程式碼能夠編譯成功並產生我們希望在範例 10-20 的 main
函式中得到的結果。
此函式簽名告訴 Rust 它有個生命週期 'a
,函式的兩個參數都是字串切片,並且會有生命週期'a
。此函式簽名還告訴了 Rust 從函式回傳的字串切片也會和生命週期 'a
存活的一樣久。實際上它代表 longest
函式回傳引用的生命週期與傳入時字串長度較短的引用的生命週期一樣。這些約束是我們希望 Rust 去強制執行的。記住當我們在此函式簽名指定生命週期參數時,我們不會變更任何傳入或傳出數值的生命週期。我們只是告訴借用檢查器應該要拒絕任何沒有服從這些約束的數值。注意到 longest
函式不需要知道 x
和 y
實際上會活多久,只需要知道有某個作用域會用 'a
取代來滿足此簽名。
當要在函式詮釋生命週期時,詮釋會位於函式簽名中,而不是函式本體。Rust 可以不用任何協助就能分析函式中的程式碼。然而當函式擁有傳入或傳出外部程式碼的引用時,Rust 無法自己判別出參數與回傳值的生命週期。每次函式呼叫時的生命週期可能都不一樣。這就是為何我們得親自詮釋生命週期。
當我們向 longest
傳入實際引用時,'a
實際替代的生命週期為 x
作用域與 y
作用域重疊得部分。換句話說,泛型生命週期 'a
取得的生命週期會等於 x
與 y
的生命週期中較短的。因為我們將回傳引用詮釋了相同的生命週期參數 'a
,回傳引用的生命週期也會保證在 x
和 y
的生命週期較短的結束前有效。
讓我們來看看如何透過傳入不同實際生命週期的引用來使生命週期詮釋能約束 longest
函式,如範例 10-23 所示。
檔案名稱:src/main.rs
fn main() { let string1 = String::from("很長的長字串"); { let string2 = String::from("xyz"); let result = longest(string1.as_str(), string2.as_str()); println!("最長的字串為 {}", result); } } fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } }
範例 10-23使用 longest
函式並傳入 String
數值的引用,但兩個參數的實際生命週期均不相同
在此例中 string1
在外部作用域結束前都有效,而 string2
在內部作用域結束前都有效,然後 result
會取得某個有效引用直到內部作用域結束為止。執行此程式的話,你會看到借用檢查器認可此程式碼,它會編譯成功然後印出 最長的字串為 很長的長字串
。
接下來,讓我們寫一個範例能要求 result
生命週期的引用必須是兩個引數中較短的才行。我們會移動變數 result
的宣告到外部作用域,但保留變數 result
的賦值與 string2
一樣在內部作用域。然後我們也將使用到 result
的 println!
移到外部作用域,緊接在內部作用域結束之後。如範例 10-24 所示,此程式碼會編譯不過。
檔案名稱:src/main.rs
fn main() {
let string1 = String::from("很長的長字串");
let result;
{
let string2 = String::from("xyz");
result = longest(string1.as_str(), string2.as_str());
}
println!("最長的字串為 {}", result);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
範例 10-24:嘗試在 string2
離開作用域後使用 result
當我們嘗試編譯此程式碼,我們會看到以下錯誤:
$ 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!("最長的字串為 {}", result);
| ------ borrow later used here
error: aborting due to previous error
For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10`.
To learn more, run the command again with --verbose.
錯誤訊息表示要讓 result
在 println!
陳述式有效的話,string2
必須在外部作用域結束前都是有效的。Rust 會知道是因為我們在函式的參數與回傳值使用相同的生命週期 'a
來詮釋。
身為人類我們能看出此程式碼的 string1
字串長度的確比 string2
長,因此 result
會包含 string1
的引用。因為 string1
尚未離開作用域,所以 string1
的引用在 println!
陳述式中仍然是有效的才對。然而編譯器在此情形會無法看出引用是有效的。所以我們才告訴 Rust longest
函式回傳引用的生命週期等同於傳入引用中較短的生命週期。這樣一來借用檢查器就會否決範例 10-24 的程式碼,因為它可能會有無效的引用。
歡迎嘗試設計更多採用不同數值與不同生命週期的引用作為 longest
函式參數與回傳值的實驗,並在編譯前假設你的實驗會不會通過借用檢查器,然後看看你的理解是不是正確的!
深入理解生命週期
你要指定生命週期參數的方式取決於函式的行為。舉例來說如果我們改變函式 longest
的實作為永遠只回傳第一個參數而不是最長的字串切片,我們就不需要在參數 y
指定生命週期。以下的程式碼就能編譯:
檔案名稱:src/main.rs
fn main() { let string1 = String::from("abcd"); let string2 = "efghijklmnopqrstuvwxyz"; let result = longest(string1.as_str(), string2); println!("最長的字串為 {}", result); } fn longest<'a>(x: &'a str, y: &str) -> &'a str { x }
在此例中,我們指定生命週期參數 'a
給參數 x
與回傳型別,但參數 y
則沒有,因為 y
的生命週期與 x
和回傳型別的生命週期之間沒有任何關係。
當函式回傳引用時,回傳型別的生命週期參數必須符合其中一個參數的生命週期參數。如果回傳引用沒有和任何參數有關聯的話,代表它引用的是函式本體中的數值。但這會是迷途引用,因為該數值會在函式結尾離開作用域。請看看以下嘗試在函式 longest
的實作做法,它並不會編譯成功:
檔案名稱:src/main.rs
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("最長的字串為 {}", result);
}
fn longest<'a>(x: &str, y: &str) -> &'a str {
let result = String::from("超長的字串");
result.as_str()
}
我們在這邊雖然有對回傳型別指定生命週期參數 'a
,但此實作還是會失敗,因為回傳值的生命週期與參數的生命週期完全無關。以下是我們獲得的錯誤訊息:
$ cargo run
Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0515]: cannot return value referencing local variable `result`
--> src/main.rs:11:5
|
11 | result.as_str()
| ------^^^^^^^^^
| |
| returns a value referencing data owned by the current function
| `result` is borrowed here
error: aborting due to previous error
For more information about this error, try `rustc --explain E0515`.
error: could not compile `chapter10`.
To learn more, run the command again with --verbose.
問題在於 result
會離開作用域並在 longest
函式結尾被清除。我們卻嘗試從函式中回傳 result
的引用。我們無法指定生命週期參數來改變迷途引用,而且 Rust 不會允許我們將建立迷途引用。在此例中,最好的解決辦法是回傳有所有權的資料型別而非引用,並讓呼叫的函式自行決定如何清理數值。
總結來說,生命週期語法是用來連接函式中不同參數與回傳值的生命週期。一旦連結起來,Rust 就可以獲得足夠的資訊來確保記憶體安全的運算並防止會產生迷途指標或違反記憶體安全的操作。
結構體定義中的生命週期詮釋
目前為止,我們只定義過擁有所有權的結構體。結構體其實也能持有引用,不過我們會需要在結構體定義中每個引用加上生命週期詮釋。範例 10-25 有個持有字串切片的結構體 ImportantExcerpt
。
檔案名稱:src/main.rs
struct ImportantExcerpt<'a> { part: &'a str, } fn main() { let novel = String::from("叫我以實瑪利。多年以前..."); let first_sentence = novel.split('.').next().expect("找不到'.'"); let i = ImportantExcerpt { part: first_sentence, }; }
範例 10-25:擁有引用的結構體,所以它的定義需要加上生命週期詮釋
此結構體有個欄位 part
並擁有字串切片引用。如同泛型資料型別,我們在結構體名稱之後的尖括號內宣告泛型生命週期參數,所以我們就可以在結構體定義的本體中使用生命週期參數。此詮釋代表 ImportantExcerpt
的實例不能比它持有的欄位 part
活得還久。
main
函式在此產生一個結構體 ImportantExcerpt
的實例並持有一個引用,其為變數 novel
所擁有的 String
中的第一個句子的引用。novel
的資料是在 ImportantExcerpt
實例之前建立的。除此之外,novel
在 ImportantExcerpt
離開作用域之前不會離開作用域,所以 ImportantExcerpt
實例中的引用是有效的。
生命週期省略
你已經學到了每個引用都有個生命週期,而且你需要在有使用引用的函式與結構體中指定生命週期參數。然而在第四章的範例 4-9 我們有函式可以不詮釋生命週期並照樣編譯成功,我們在範例 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[..] } fn main() { let my_string = String::from("hello world"); // first_word works on slices of `String`s let word = first_word(&my_string[..]); let my_string_literal = "hello world"; // first_word works on slices of string literals let word = first_word(&my_string_literal[..]); // Because string literals *are* string slices already, // this works too, without the slice syntax! let word = first_word(my_string_literal); }
範例 10-26:在範例 4-9 定義過的函式,雖然其參數與回傳值均為引用,卻仍可編譯成功
此函式可以不用生命週期詮釋仍照樣編譯過是有歷史因素的:在早期版本的 Rust(1.0 之前),此程式碼是無法編譯的,因為每個引用都得有顯式生命週期。在當時的情況下,此函式簽名會長得像這樣:
fn first_word<'a>(s: &'a str) -> &'a str {
在寫了大量的 Rust 程式碼後,Rust 團隊發現 Rust 開發者會在特定情況反覆輸入同樣的生命週期詮釋。這些情形都是可預期的,而且可以遵循一些明確的模式。開發者將這些模式加入編譯器的程式碼中,所以借用檢查器可以依據這些情況自行推導生命週期,而讓我們不必顯式詮釋。
這樣的歷史值得提起的原因是因為很可能會有更多明確的模式被找出來並加到編譯器中,意味著未來對於生命週期詮釋的要求會更少。
被寫進 Rust 引用分析的模式被稱作生命週期省略規則(lifetime elision rules)。這些不是程式設計師要遵守的規則,而是一系列編譯器能去考慮的情形。而如果你的程式碼符合這些情形時,你就不必顯式寫出生命週期。
省略規則無法提供完整的推導。如果 Rust 能明確套用規則,但在這之後還是有引用存在模棱兩可的生命週期,編譯器就無法猜出剩餘引用的生命週期。在此情況,編譯器不會亂猜,它會回傳錯誤給你,你可以指定生命週期詮釋來指明引用之間的關係。
在函式或方法參數上的生命週期稱為輸入生命週期(input lifetimes),而在回傳值的生命週期則稱為輸出生命週期(output lifetimes)。
當引用沒有顯式詮釋生命週期時,編譯器會用三項規則來推導它們。第一個規則適用於輸入生命週期,而第二與第三個規則適用於輸出生命週期。如果編譯器處理完這三個規則,卻仍有引用無法推斷出生命週期時,編譯器就會停止並回傳錯誤。適用於 fn
定義的規則一樣適用於 impl
區塊。
第一個規則是每個引用都會有自己的生命週期參數。換句話說,一個函式只有一個參數的話,就只會有一個生命週期:fn foo<'a>(x: &'a i32)
;一個函式有兩個參數的話,就會有分別兩個生命週期參數:fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
,以此類推。
第二個規則是如果剛好只有一個輸入生命週期參數,該參數就會賦值給所有輸出生命週期參數:fn foo<'a>(x: &'a i32) -> &'a i32
。
第三個規則是如果有多個輸入生命週期參數,但其中一個是 &self
或 &mut self
,由於這是方法,self
的生命週期會賦值給所有輸出生命週期參數。此規則讓方法更容易讀寫,因為不用寫更多符號出來。
讓我們假裝我們是編譯器。我們會檢查這些規則並找出範例 10-26 中函式 first_word
簽名中引用的生命週期。簽名的引用一開始沒有任何生命週期:
fn first_word(s: &str) -> &str {
接著編譯器檢查第一個規則,指明每個參數都有自己的生命週期。我們如往常一樣指定 'a
,所以簽名就會變成:
fn first_word<'a>(s: &'a str) -> &str {
然後第二個規則也是用因為這裡剛好就一個輸入生命週期而已。第二個規則指明只有一個輸入生命週期的話,就會賦值給所有其他輸出生命週期。所以簽名現在變成這樣:
fn first_word<'a>(s: &'a str) -> &'a str {
現在此函式所有的引用都有生命週期了,而且編譯器可以繼續分析,不必要求程式設計師在此詮釋函式簽名的生命週期。
讓我們再看看一個例子,這次是範例 10-21 一開始沒有任何生命週期參數的 longest
函式:
fn longest(x: &str, y: &str) -> &str {
讓我們先檢查第一項規則:每個參數都有自己的生命週期。這次我們有兩個參數,所以我們有兩個生命週期:
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
你可以看出來第二個規則並不適用,因為我們有不止一個輸入生命週期。而第三個也不適用,因為 longest
是函式而非方法,其參數不會有 self
。遍歷這三個規則下來,我們仍然無法推斷出回傳型別的生命週期。這就是為何我們嘗試編譯範例 10-21 的程式碼會出錯的原因:編譯器遍歷生命週期省略規則,但仍然無法推導出簽名中所有引用的生命週期。
因為第三個規則僅適用於方法簽名,我們接下來就會看看這種情況時的生命週期,看看為何第三個規則讓我們不必常常在方法簽名詮釋生命週期。
在方法定義中的生命週期詮釋
當我們在有生命週期的結構體上實作方法時,其語法類似於我們在範例 10-11 中泛型型別參數的語法。 宣告並使用生命週期參數的地方會依據它們是否與結構體欄位或方法參數與回傳值相關。
結構體欄位的生命週期永遠需要宣告在 impl
關鍵字後方以及結構體名稱後方,因為這些生命週期是結構體型別的一部分。
在 impl
區塊中方法簽名的引用可能會與結構體欄位的引用生命週期綁定,或者它們可能是互相獨立的。除此之外,生命週期省略規則常常可以省略方法簽名中的生命週期詮釋。讓我們看看範例 10-25 定義過的 ImportantExcerpt
來作為範例。
首先我們使用一個方叫做 level
其參數只有 self
的引用而回傳值是 i32
,這不是任何引用:
struct ImportantExcerpt<'a> { part: &'a str, } impl<'a> ImportantExcerpt<'a> { fn level(&self) -> i32 { 3 } } impl<'a> ImportantExcerpt<'a> { fn announce_and_return_part(&self, announcement: &str) -> &str { println!("請注意:{}", announcement); self.part } } fn main() { let novel = String::from("叫我以實瑪利。多年以前..."); let first_sentence = novel.split('.').next().expect("找不到'.'"); let i = ImportantExcerpt { part: first_sentence, }; }
生命週期參數宣告在 impl
之後,而且也要在型別名稱之後加上。但是我們不必在 self
的引用加上生命週期詮釋,因為其適用於第一個省略規則。
以下是第三個生命週期省略規則適用的地方:
struct ImportantExcerpt<'a> { part: &'a str, } impl<'a> ImportantExcerpt<'a> { fn level(&self) -> i32 { 3 } } impl<'a> ImportantExcerpt<'a> { fn announce_and_return_part(&self, announcement: &str) -> &str { println!("請注意:{}", announcement); self.part } } fn main() { let novel = String::from("叫我以實瑪利。多年以前..."); let first_sentence = novel.split('.').next().expect("找不到'.'"); let i = ImportantExcerpt { part: first_sentence, }; }
這裡有兩個輸入生命週期,所以 Rust 用第一個生命週期省略規則給予 &self
和 announcement
它們自己的生命週期。然後因為其中一個參數是 &self
,回傳型別會取得 &self
的生命週期,如此一來所有的生命週期都推導出來了。
靜態生命週期
其中有個特殊的生命週期 'static
我們需要進一步討論,這是指該引用可以存活在整個程式期間。所有的字串字面值都有 'static
生命週期,我們可以這樣詮釋:
#![allow(unused)] fn main() { let s: &'static str = "我有靜態生命週期。"; }
此字串的文字會直接儲存在程式的二進制檔案中,所以永遠有效。因此所有的字串字面值的生命週期都是 'static
。
你有時可能會看到錯誤訊息建議使用 'static
生命週期。但在你對引用指明 'static
生命週期前,最好想一下該引用的生命週期是否真的會存在於整個程式期間。就算它可以,你可能也得考慮是不是該活得這麼久。大多數的情況,程式問題都來自於嘗試建立迷途引用或可用的生命週期不符。這樣的情況下,應該是要實際嘗試解決問題,而不是指明 'static
生命週期。
組合泛型型別參數、特徵界限與生命週期
讓我們用一個函式來總結泛型型別參數、特徵界限與生命週期的語法!
fn main() { let string1 = String::from("abcd"); let string2 = "xyz"; let result = longest_with_an_announcement( string1.as_str(), string2, "Today is someone's birthday!", ); println!("最長的字串為 {}", result); } 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!("公告!{}", ann); if x.len() > y.len() { x } else { y } }
這是範例 10-22 會回傳兩個字串切片較長者的 longest
函式。不過現在它有個額外的參數 ann
,使用的是泛型型別 T
,它可以是任何在 where
中所指定有實作 Display
特徵的型別。此額外參數會在函式比較兩個字串切片前印出來,這也是為何需要 Display
特徵界限。因為生命週期也是一種泛型,生命週期參數 'a
與泛型型別參數 T
都宣告在函式名稱後的尖括號內。
總結
我們在此章節涵蓋了許多內容!現在你已經知道泛型型別參數、特徵與特徵界限以及泛型生命週期參數,你已經準備好能寫出適用於許多不同情況且不重複的程式碼了。泛型型別參數讓你可以讓程式碼適用於不同型別;特徵與特徵界限確保就算型別為泛型,它們都會有相同的行為。你還學到了使用生命週期詮釋確保此如此彈性的程式碼不會造成迷途引用。而且這些分析都發生在編譯期間,完全不影響執行時效能!
不管你信不信,本章節還有很多延伸主題可以導論,像是第十七章就會討論特徵物件(trait objects),這是另一個使用特徵的方法。另外還有一些更複雜的場合會涉及到更進階的生命週期詮釋。對此你可能就會想閱讀 Rust Reference。接下來,你將學習如何在 Rust 寫測試,讓你可以確保程式碼能如期執行。
- translators: [Ngô͘ Io̍k-ūi [email protected]]
- commit: e5ed971
- updated: 2020-09-15