生命週期
生命週期的意思是"變數存活得有多久"。你只需要思考參考的生命週期。這是因為參考不能存活得比它們所來自的物件更久。例如說這個函式就不能執行:
fn returns_reference() -> &str { let my_string = String::from("I am a string"); &my_string // ⚠️ } fn main() {}
問題在於 my_string
只存活在 returns_reference
的範圍裡。我們試著回傳 &my_string
,但是 &my_string
不能存在於沒有 my_string
的地方。所以編譯器會說不行。
這段程式碼也不能執行。
fn returns_str() -> &str { let my_string = String::from("I am a string"); "I am a str" // ⚠️ } fn main() { let my_str = returns_str(); println!("{}", my_str); }
但是幾乎要成功了。編譯器卻說:
error[E0106]: missing lifetime specifier
--> src\main.rs:6:21
|
6 | fn returns_str() -> &str {
| ^ expected named lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
|
6 | fn returns_str() -> &'static str {
| ^^^^^^^^
missing lifetime specifier
的意思是,我們需要加上表示生命週期的 '
。然後它說 contains a borrowed value, but there is no value for it to be borrowed from
。也就是說,I am a str
不是借來的。它說 consider using the 'static lifetime
要寫成 &'static str
。因此它認為我們應該嘗試說這是個字串字面常數。
現在可以執行了:
fn returns_str() -> &'static str { let my_string = String::from("I am a string"); "I am a str" } fn main() { let my_str = returns_str(); println!("{}", my_str); }
這是因為我們回傳了生命週期是 static
的 &str
。同時,my_string
只能以 String
的型別回傳:我們不能回傳對它的參考,因為它將在下一行死亡。
所以現在 fn returns_str() -> &'static str
告訴Rust,"別擔心,我們只會回傳字串字面常數"。字串字面常數存活在整個程式中,所以 Rust 很高興。你會注意到,這與泛型類似。當我們告訴編譯器像似 <T: Display>
的東西時,我們承諾的是我們將只會使用有 Display
特徵的輸入。生命週期也類似:我們並沒有改變任何變數的生命週期。我們只是告訴編譯器輸入的生命週期會是什麼。
但是 'static
並不是唯一的生命週期。實際上,每個變數都有一個生命週期,但通常我們不必寫出來。編譯器很聰明,通常都能自己想出來。只有在編譯器不知道的時候,我們才需要去寫出生命週期。
這是另一個生命週期的範例。想像一下,我們想建立 City
結構體,並給它 &str
的名字。我們可能想這樣做是因為效能比用 String
還快。所以我們寫成這樣,但還不能執行:
#[derive(Debug)] struct City { name: &str, // ⚠️ date_founded: u32, } fn main() { let my_city = City { name: "Ichinomiya", date_founded: 1921, }; }
編譯器說:
error[E0106]: missing lifetime specifier
--> src\main.rs:3:11
|
3 | name: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
2 | struct City<'a> {
3 | name: &'a str,
|
Rust 需要 &str
的生命週期,因為 &str
是個參考。如果 name
指向的值被丟棄 (drop) 了會怎樣?那就不安全 (unsafe) 了。
那麼 'static
呢,能用嗎?我們以前用過。讓我們試試吧:
#[derive(Debug)] struct City { name: &'static str, // 把 &str 改成 &'static str date_founded: u32, } fn main() { let my_city = City { name: "Ichinomiya", date_founded: 1921, }; println!("{} was founded in {}", my_city.name, my_city.date_founded); }
好的,這就可以了。也許這就是你想要的結構體。不過,要注意我們只能接受"字串字面常數",所以不能接受對其他東西的參考。所以這將無法執行:
#[derive(Debug)] struct City { name: &'static str, // 一定要在整個程式裡存活 date_founded: u32, } fn main() { let city_names = vec!["Ichinomiya".to_string(), "Kurume".to_string()]; // city_names 沒有存活在整個程式 let my_city = City { name: &city_names[0], // ⚠️ 這是個 &str, 但不是 &'static str. 這是對 city_names 裡面的值的參考 date_founded: 1921, }; println!("{} was founded in {}", my_city.name, my_city.date_founded); }
編譯器說:
error[E0597]: `city_names` does not live long enough
--> src\main.rs:12:16
|
12 | name: &city_names[0],
| ^^^^^^^^^^
| |
| borrowed value does not live long enough
| requires that `city_names` is borrowed for `'static`
...
18 | }
| - `city_names` dropped here while still borrowed
這一點很重要,因為我們給它的參考其實活得夠久了。但是我們承諾的只有給它 &'static str
,這就是問題所在。
所以現在我們就試試之前編譯器的建議。它說嘗試寫成 struct City<'a>
和 name: &'a str
。這就意味著,只有當 name
活得和 City
一樣久的情況下,它才會接受 name
的參考。
#[derive(Debug)] struct City<'a> { // City 的生命週期是 'a name: &'a str, // 且 name 的生命週期也是 'a. date_founded: u32, } fn main() { let city_names = vec!["Ichinomiya".to_string(), "Kurume".to_string()]; let my_city = City { name: &city_names[0], date_founded: 1921, }; println!("{} was founded in {}", my_city.name, my_city.date_founded); }
另外要記住,如果你願意你可以寫任何東西來代替 'a
。這也和在泛型裡我們寫 T
和 U
時類似,但實際上可以寫任何東西。
#[derive(Debug)] struct City<'city> { // 這裡的生命週期名稱叫做 'city name: &'city str, // 並且 name 有著 'city 生命週期 date_founded: u32, } fn main() {}
所以通常都會寫做 'a, 'b, 'c
等等,因為這是快速且常用的寫法。但如果你想的話,你永遠都可以更改。有個好建議是,把生命週期名稱改成 "人類可讀(human-readable)" 的名字有助於閱讀理解程式碼,尤其是程式碼非常複雜時。
讓我們再來看看與用在泛型的特徵的比較。比如說:
use std::fmt::Display; fn prints<T: Display>(input: T) { println!("T is {}", input); } fn main() {}
當你寫 T: Display
的時候,它的意思是"只有在 T 有 Display 時,才接受 T"。
而不是說:"我把 Display 給予 T"。
對於生命週期也是如此。當你在這裡寫 'a
:
#[derive(Debug)] struct City<'a> { name: &'a str, date_founded: u32, } fn main() {}
意思是"如果 name
的生命週期至少與 City
一樣久,才接受 name
的輸入"。
它的意思不是說:"我會讓 name
的輸入與 City
活得一樣久"。
現在我們可以學到有關先前見過的 <'_>
。這被稱為"匿名生命週期",它是參考被使用時的指示器。例如,當你在實現結構時,Rust 會向你建議使用。這裡有個幾乎可以但還不能用的結構體:
// ⚠️ struct Adventurer<'a> { name: &'a str, hit_points: u32, } impl Adventurer { fn take_damage(&mut self) { self.hit_points -= 20; println!("{} has {} hit points left!", self.name, self.hit_points); } } fn main() {}
所以我們對 struct
做了我們需要做的事情:首先我們說 name
來自於 &str
。這就意味著我們需要生命週期,所以我們給了它 <'a>
。然後我們必須對 struct
做同樣的處理,以證明它們至少和這個生命週期一樣久。但是 Rust 卻告訴我們要這樣做:
error[E0726]: implicit elided lifetime not allowed here
--> src\main.rs:6:6
|
6 | impl Adventurer {
| ^^^^^^^^^^- help: indicate the anonymous lifetime: `<'_>`
它想讓我們加上那個匿名生命週期,以表明有個參考被使用。所以如果我們這樣寫,它就會很高興:
struct Adventurer<'a> { name: &'a str, hit_points: u32, } impl Adventurer<'_> { fn take_damage(&mut self) { self.hit_points -= 20; println!("{} has {} hit points left!", self.name, self.hit_points); } } fn main() {}
這個生命週期是為了讓你不必總是寫諸如 impl<'a> Adventurer<'a>
這樣的東西,因為結構體已經寫出了生命週期。
在 Rust 中,生命週期是可以很困難的,但這裡有一些技巧可以在面對它們時避免感到太大的壓力:
- 如果你想在當下避免它們,你可以繼續使用擁有所有權的型別,使用克隆等。
- 很多時候,當編譯器想要生命週期的時候,到頭來你只要在這裡和那裡寫上
<'a>
就可以用了。這只是一種"別擔心,我不會給你任何活得不夠久的東西"的說法。 - 你可以每次只探索生命週期一些些。寫一些擁有所有權的數值的程式碼,然後把其中一個變成參考。編譯器會開始抱怨,但也會給出一些建議。如果它變得太複雜,你可以撤銷它,下次再試。
讓我們用我們的程式碼來這麼做,看看編譯器會怎麼說。首先我們回去把生命週期去掉,同時也實作 Display
。Display
就會印出 Adventurer
的名字。
// ⚠️ struct Adventurer { name: &str, hit_points: u32, } impl Adventurer { fn take_damage(&mut self) { self.hit_points -= 20; println!("{} has {} hit points left!", self.name, self.hit_points); } } impl std::fmt::Display for Adventurer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{} has {} hit points.", self.name, self.hit_points) } } fn main() {}
第一個抱怨就是這個:
error[E0106]: missing lifetime specifier
--> src\main.rs:2:11
|
2 | name: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 | struct Adventurer<'a> {
2 | name: &'a str,
|
它建議這麼做:在 Adventurer 後面加上 <'a>
,以及 &'a str
。所以我們照著做:
// ⚠️ struct Adventurer<'a> { name: &'a str, hit_points: u32, } impl Adventurer { fn take_damage(&mut self) { self.hit_points -= 20; println!("{} has {} hit points left!", self.name, self.hit_points); } } impl std::fmt::Display for Adventurer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{} has {} hit points.", self.name, self.hit_points) } } fn main() {}
現在它對那些部分很滿意了,但對 impl
區塊不太確定。它想要我們提示正在使用參考:
error[E0726]: implicit elided lifetime not allowed here
--> src\main.rs:6:6
|
6 | impl Adventurer {
| ^^^^^^^^^^- help: indicate the anonymous lifetime: `<'_>`
error[E0726]: implicit elided lifetime not allowed here
--> src\main.rs:12:28
|
12 | impl std::fmt::Display for Adventurer {
| ^^^^^^^^^^- help: indicate the anonymous lifetime: `<'_>`
好了,我們將這些寫進去......現在它通過編譯了!現在我們可以做出 Adventurer
,然後用它做些事。
struct Adventurer<'a> { name: &'a str, hit_points: u32, } impl Adventurer<'_> { fn take_damage(&mut self) { self.hit_points -= 20; println!("{} has {} hit points left!", self.name, self.hit_points); } } impl std::fmt::Display for Adventurer<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{} has {} hit points.", self.name, self.hit_points) } } fn main() { let mut billy = Adventurer { name: "Billy", hit_points: 100_000, }; println!("{}", billy); billy.take_damage(); }
印出:
Billy has 100000 hit points.
Billy has 99980 hit points left!
所以你可以看到,編譯器往往只是想要確定生命週期。而且它通常很聰明,幾乎可以猜到你想要的生命週期,只是需要你告訴它,它就可以確定了。