Rust 學習筆記:生命週期
2023年10月22日 · 中文
前言
本系列文書寫我個人初探 Rust 的學習筆記,章節劃分主要基於著名的 The Book,The Rust Programming Language,程式碼部分通常是個人閱讀消化後以方便說明的方式撰寫,完整學習建議直接參見該書。
The Rust Programming Language
該書也有中文翻譯版,不過個人閱讀以英文原版為主以鞏固對 terminology 的一致認識,我認為對未來閱讀以及查找資料會較為順暢。
不論語言該書都是相當優秀的學習資源,選擇適合你的語言開始學習 Rust 吧。
Appendix F: Translations of the Book
生命週期 lifetimes
Lifetime 是 Rust 另一大困難的 topic,Rust 透過 borrow checker 來避免出現迷途參考以確保安全性,通常情況下 lifetime 都是隱式地被 infer 出來的,但還是有情況會是需要顯式地告知 borrow checker 參考的生命週期,能夠更彈性地設計程式。
指定生命週期
以下函式執行時會報錯,因為這邊 Rust 無法 infer 出生命週期,認為有可能出現迷途參考,error message 也很清楚的說明需要指定生命週期。
fn main() { let s1 = String::from("Hello"); let s2 = String::from("World"); let s = get_back_str(s1.as_str(), s2.as_str()); // ERROR: missing lifetime specifier println!("{s}"); } fn get_back_str(s1: &str, s2: &str) -> &str { s1 }
指定生命週期使用 '
(tick),一般的命名慣例是以 'a
作為第一個生命週期的名稱。
fn main() { let s1 = String::from("Hello"); let s2 = String::from("World"); let s = get_back_str(s1.as_str(), s2.as_str()); println!("{s}"); // Hello } fn get_back_str<'a>(s1: &'a str, s2: &'a str) -> &'a str { s1 }
也可以給予複數不同的生命週期,並指定回傳值的生命週期。
fn main() { let s1 = String::from("Hello"); let s; { let s2 = String::from("World"); s = get_back_str(s1.as_str(), s2.as_str()); } println!("{s}"); // Hello } fn get_back_str<'a, 'b>(s1: &'a str, s2: &'b str) -> &'a str { s1 }
若將上面回傳值的生命週期改為 'b
便會報錯,錯誤訊息也很清楚告訴我們 s2
存活的並不夠長。
fn main() { let s1 = String::from("Hello"); let s; { let s2 = String::from("World"); s = get_back_str(s1.as_str(), s2.as_str()); } println!("{s}"); // ERROR: `s2` does not live long enough } fn get_back_str<'a, 'b>(s1: &'a str, s2: &'b str) -> &'b str { s1 }
而如果上面程式不指定兩個生命週期僅用一個,Rust 會使用 s1
、s2
兩者中生命週期較短的一個做為 'a
,因此會得到一樣的錯誤。
fn main() { let s1 = String::from("Hello"); let s; { let s2 = String::from("World"); s = get_back_str(s1.as_str(), s2.as_str()); } println!("{s}"); // ERROR: `s2` does not live long enough } fn get_back_str<'a>(s1: &'a str, s2: &'a str) -> &'a str { s1 }
在 struct 中指定生命週期
我們也可以透過指定生命週期讓參考作為 struct
的資料。
#[derive(Debug)] struct User<'a> { name: &'a str } fn main() { let u1_name = String::from("Henry"); let u1 = User { name: u1_name.as_str() }; let u2; { let u2_name = String::from("Josh"); u2 = User { name: u2_name.as_str() }; println!("{:?}", u2); // User { name: "Josh" } } println!("{:?}", u1); // User { name: "Henry" } }
如預期地,在 u2_name
被釋放之後取用 u2
便會報錯。
fn main() { let u1_name = String::from("Henry"); let u1 = User { name: u1_name.as_str() }; let u2; { let u2_name = String::from("Josh"); u2 = User { name: u2_name.as_str() }; } println!("{:?}", u2); // ERROR: `u2_name` does not live long enough }
Lifetime elision
並不是所有情況都必須顯式地指定生命週期,Rust 會根據規則嘗試 infer 生命週期,因此在很多情況下是可以省略的。規則被稱為 lifetime elision rules,有三項規則:
- compiler 會給是參考的參數一個生命週期。
- 如果剛好只有一個生命週期參數,Rust 會將該生命週期指定給回傳值。
- 如果有複數個生命週期參數,但其中一個是
&self
或&mut self
,Rust 會將該生命週期賦予回傳值。
在適用三項規則後仍無法確定回傳值的生命週期時,Rust 的 compiler 只好拋出錯誤,提示需要手動指定生命週期。
此時回到本篇第一段程式碼,get_back_str
函式沒有指定生命週期,Rust 嘗試以規則推導:
fn main() { let s1 = String::from("Hello"); let s2 = String::from("World"); let s = get_back_str(s1.as_str(), s2.as_str()); // ERROR: missing lifetime specifier println!("{s}"); } fn get_back_str(s1: &str, s2: &str) -> &str { s1 }
根據第一項規則推導後,函式是這個樣子的
fn get_back_str<'a, 'b>(s1: &'a str, s2: &'b str) -> &str
因為有兩個生命週期,第二項規則不適用,因為沒有 &self
這並非 method,也不適用第三項規則,此時 Rust 仍然無法推導回傳值的生命週期,因而拋出錯誤。
static lifetime
Rust 一個特別的生命週期 'static
(靜態生命週期),表示存活在整個程式期間,這也是 string literal 的生命週期,因此 string literal 一直都會有效 (always available)。
let s: &'static str = "Hello";
此時來看看上面這段會報錯的程式:
fn main() { let s1 = String::from("Hello"); let s; { let s2 = String::from("World"); s = get_back_str(s1.as_str(), s2.as_str()); } println!("{s}"); // ERROR: `s2` does not live long enough } fn get_back_str<'a>(s1: &'a str, s2: &'a str) -> &'a str { s1 }
若把 s1
和 s2
改成 string literal 的話,就不會報錯,顯見 s2
仍是有效的。
fn main() { let s1 = "Hello"; let s; { let s2 = "World"; s = get_back_str(s1, s2); } println!("{s}"); // Hello } fn get_back_str<'a>(s1: &'a str, s2: &'a str) -> &'a str { s1 }