生命週期

生命週期的意思是"變數存活得有多久"。你只需要思考參考的生命週期。這是因為參考不能存活得比它們所來自的物件更久。例如說這個函式就不能執行:

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。這也和在泛型裡我們寫 TU 時類似,但實際上可以寫任何東西。

#[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> 就可以用了。這只是一種"別擔心,我不會給你任何活得不夠久的東西"的說法。
  • 你可以每次只探索生命週期一些些。寫一些擁有所有權的數值的程式碼,然後把其中一個變成參考。編譯器會開始抱怨,但也會給出一些建議。如果它變得太複雜,你可以撤銷它,下次再試。

讓我們用我們的程式碼來這麼做,看看編譯器會怎麼說。首先我們回去把生命週期去掉,同時也實作 DisplayDisplay 就會印出 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!

所以你可以看到,編譯器往往只是想要確定生命週期。而且它通常很聰明,幾乎可以猜到你想要的生命週期,只是需要你告訴它,它就可以確定了。