透過生命週期驗證參考

生命週期(lifetime)是另一種我們已經使用過的泛型。不同於確保一個型別有沒有我們要的行為,生命週期確保我們在需要參考的時候,它們都是有效的。

我們在第四章的「參考與借用」段落沒談到的是,Rust 中的每個參考都有個生命週期,這是決定該參考是否有效的作用域。大多情況下生命週期是隱式且可推導出來的,就像大多情況下型別是可推導出來的。當多種型別都有可能時,我們就得詮釋型別。同樣地,當生命週期的參考能以不同方式關聯的話,我們就得詮釋生命週期。Rust 要求我們用泛型生命週期參數來詮釋參考之間的關係,以確保實際在執行時的參考絕對是有效的。

詮釋生命週期在大多數的程式語言中都沒有這個概念,所以這段可能會有點讓你覺得陌生。雖然我們不會在此章涵蓋所有生命週期的內容,但是我們會講些你可能遇到生命週期的常見場景,好讓你更加熟悉這個概念。

透過生命週期預防迷途參考

生命週期最主要的目的就是要預防迷途參考(dangling references),其會導致程式參考到其他資料,而非它原本想要的參考。請看一下範例 10-16 的程式,它有一個外部作用域與內部作用域。

fn main() {
    let r;

    {
        let x = 5;
        r = &x;
    }

    println!("r: {}", r);
}

範例 10-16:嘗試使用其值已經離開作用域的參考

注意:範例 10-16、10-17 與 10-23 宣告變數時都沒有給予初始數值,所以變數名稱可以存在於外部作用域。乍看之下這似乎違反 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:6:13
  |
6 |         r = &x;
  |             ^^ borrowed value does not live long enough
7 |     }
  |     - `x` dropped here while still borrowed
8 |
9 |     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 「存在的不夠久」。原因是因為當內部作用域在第 7 行結束時,x 會離開作用域。但是 r 卻還在外部作用域中有效,我們會說的「活得比較久」。如果 Rust 允許此程式碼可以執行的話,r 就會參考到 x 離開作用域後被釋放的記憶體位置,然後我們嘗試對 r 做的事情都不會是正確的了。所以 Rust 如何決定此程式碼無效呢?它使用了借用檢查器。

借用檢查器

Rust 編譯器有個借用檢查器(borrow checker)會比較作用域來檢測所有的借用是否有效。範例 10-17 顯示了範例 10-16 的程式碼,但加上了變數生命週期的詮釋。

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+

範例 10-17:變數 rx 的生命週期詮釋,分別以 'a'b 作為表示

我們在此定義 r 的生命週期詮釋為 'ax 的生命週期為 'b。如同你所見,內部的 'b 區塊比外部的 'a 生命週期區塊還小。在編譯期間,Rust 會比較兩個生命週期的大小,並看出 r 有生命週期 'a 但它參考的記憶體有生命週期 'b。程式被回絕的原因是因為 'b'a 還短:被參考的對象比參考者存在的時間還短。

範例 10-18 修正了此程式碼讓它不會存在迷途參考,並能夠正確編譯。

fn main() {
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {}", r); //   |       |
                          // --+       |
}                         // ----------+

範例 10-18:一個有效參考,因為資料比參考的生命週期還長

x 在此有生命週期 'b,此時它比 'a 還長。這代表 r 可以參考 x,因為 Rust 知道 r 的參考在 x 是有效的時候永遠是有效的。

現在你知道參考的生命週期,以及 Rust 如何分析生命週期以確保參考永遠有效了。讓我們來探索函式中參數與回傳值的泛型生命週期。

函式中的泛型生命週期

讓我們寫個回傳兩個字串切片中較長者的函式。此函式會接收兩個字串切片並回傳一個字串切片。在我們實作 longest 函式後,範例 10-19 的程式碼應該要印出 最長的字串為 abcd

檔案名稱:src/main.rs

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("最長的字串為 {}", result);
}

範例 10-19:main 函式呼叫 longest 函式來找出兩個字串切片中較長的

注意我們想要函式接收的是字串切片的參考,而不是字串,因為我們不希望 longest 函式會取得它參數的所有權。第四章的「字串切片作為參數」段落有提到為何範例 10-19 的參數正是我們所想要使用的參數。

如果我們嘗試實作 longest 函式時,如範例 10-20 所示,它不會編譯過。

檔案名稱: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-20:回傳兩個字串中較長者的 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 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

提示文字表示回傳型別需要有一個泛型生命週期參數,因為 Rust 無法辨別出回傳的參考指的是 x 還是 y。事實上,我們也不知道,因為函式本體中的 if 區塊會回傳 x 的參考而 else 區塊會回傳 y 的參考!

當我們定義函式時,我們不知道傳遞進此函式的實際數值會是什麼,所以我們不知道到底是 ifelse 的區塊會被執行。我們也不知道傳遞進來的參考實際的生命週期為何,所以我們無法像範例 10-17 和 10-18 那樣觀察作用域,來判定我們回傳的參考會永遠有效。要修正此錯誤,我們要加上泛型生命週期參數來定義參考之間的關係,讓借用檢查器能夠進行分析。

生命週期詮釋語法

生命週期詮釋(Lifetime Annotation)不會改變參考能存活多久,它們僅描述了數個參考的生命週期之間互相的關係,而不會影響其生命週期。就像當函式簽名指定了一個泛型型別參數時,函式便能夠接受任意型別一樣。函式可以指定一個泛型生命週期參數,這樣函式就能接受任何生命週期。

生命週期詮釋的語法有一點不一樣:生命週期參數的名稱必須以撇號(')作為開頭,通常全是小寫且很短,就像泛型型別一樣。大多數的人會使用名稱 'a 作為第一個生命週期詮釋。我們將生命週期參數置於參考的 & 之後,並使用空格區隔詮釋與參考的型別。

以下是一些例子:沒有生命週期參數的 i32 參考、有生命週期 'ai32 參考以及有生命週期 'ai32 可變參考。

&i32        // 一個參考
&'a i32     // 一個有顯式生命週期的參考
&'a mut i32 // 一個有顯式生命週期的可變參考

只有自己一個生命週期本身沒有多少意義,因為該詮釋是為了告訴 Rust 數個參考的泛型生命週期參數之間互相的關係。讓我們來研究生命週期詮釋如何在 longest 函式中相互關聯吧。

函式簽名中的生命週期詮釋

要在函式簽名使用生命週期詮釋的話,我們需要在函式名稱與參數列表之間的尖括號內宣告泛型生命週期參數,就像泛型型別參數那樣。

我們想在此簽名表達這樣的限制:只要所有參數都要是有效的,那麼回傳的參考才也會是有效的。也就是參數的生命週期與回傳參考的生命週期是相關的。我們會將生命週期命名為 'a 然後將它加到每個參考,如範例 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<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

範例 10-21:longest 函式定義指定所有簽名中的參考必須有相同的生命週期 'a

此程式碼能夠編譯成功並產生我們希望在範例 10-19 的 main 函式中得到的結果。

此函式簽名告訴 Rust 它有個生命週期 'a,函式的兩個參數都是字串切片,並且會有生命週期'a。此函式簽名還告訴了 Rust 從函式回傳的字串切片也會和生命週期 'a 存活的一樣久。實際上它代表 longest 函式回傳參考的生命週期與函式引數傳入時生命週期較短的參考的生命週期一樣。這樣的關係正是我們想讓 Rust 知道以便分析這段程式碼。

注意當我們在此函式簽名指定生命週期參數時,我們不會變更任何傳入或傳出數值的生命週期。我們只是告訴借用檢查器應該要拒絕任何沒有服從這些約束的數值。注意到 longest 函式不需要知道 xy 實際上會活多久,只需要知道有某個作用域會用 'a 取代來滿足此簽名。

當要在函式詮釋生命週期時,詮釋會位於函式簽名中,而不是函式本體。就像型別會寫在簽名中一樣,生命週期詮釋會成為函式的一部份。在函式簽名加上生命週期能讓 Rust 編譯器的分析工作變得更輕鬆。如果當函式的詮釋或呼叫的方式出問題時,編譯器錯誤就能限縮到我們的程式碼中指出來。如果都改讓 Rust 編譯器去推導可能的生命週期關係的話,編譯器可能會指到程式碼真正出錯之後的好幾步之後。

當我們向 longest 傳入實際參考時,'a 實際替代的生命週期為 x 作用域與 y 作用域重疊的部分。換句話說,泛型生命週期 'a 取得的生命週期會等於 xy 的生命週期中較短的。因為我們將回傳的參考詮釋了相同的生命週期參數 'a,回傳參考的生命週期也會保證在 xy 的生命週期較短的結束前有效。

讓我們來看看如何透過傳入不同實際生命週期的參考來使生命週期詮釋能約束 longest 函式,如範例 10-22 所示。

檔案名稱: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-22 使用 longest 函式並傳入 String 數值的參考,但兩個參數的實際生命週期均不相同

在此例中 string1 在外部作用域結束前都有效,而 string2 在內部作用域結束前都有效,然後 result 會取得某個有效參考直到內部作用域結束為止。執行此程式的話,你會看到借用檢查器認可此程式碼,它會編譯成功然後印出 最長的字串為 很長的長字串

接下來,讓我們寫一個範例能要求 result 生命週期的參考必須是兩個引數中較短的才行。我們會移動變數 result 的宣告到外部作用域,但保留變數 result 的賦值與 string2 一樣在內部作用域。然後我們也將使用到 resultprintln! 移到外部作用域,緊接在內部作用域結束之後。如範例 10-23 所示,此程式碼會編譯不過。

檔案名稱: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-23:嘗試在 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

For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` due to previous error

錯誤訊息表示要讓 resultprintln! 陳述式有效的話,string2 必須在外部作用域結束前都是有效的。Rust 會知道是因為我們在函式的參數與回傳值使用相同的生命週期 'a 來詮釋。

身為人類我們能看出此程式碼的 string1 字串長度的確比 string2 長,因此 result 會包含 string1 的參考。因為 string1 尚未離開作用域,所以 string1 的參考在 println! 陳述式中仍然是有效的才對。然而編譯器在此情形會無法看出參考是有效的。所以我們才告訴 Rust longest 函式回傳參考的生命週期等同於傳入參考中較短的生命週期。這樣一來借用檢查器就會否決範例 10-23 的程式碼,因為它可能會有無效的參考。

歡迎嘗試設計更多採用不同數值與不同生命週期的參考作為 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 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

問題在於 result 會離開作用域並在 longest 函式結尾被清除。我們卻嘗試從函式中回傳 result 的參考。我們無法指定生命週期參數來改變迷途參考,而且 Rust 不會允許我們將建立迷途參考。在此例中,最好的解決辦法是回傳有所有權的資料型別而非參考,並讓呼叫的函式自行決定如何清理數值。

總結來說,生命週期語法是用來連接函式中不同參數與回傳值的生命週期。一旦連結起來,Rust 就可以獲得足夠的資訊來確保記憶體安全的運算並防止會產生迷途指標或違反記憶體安全的操作。

結構體定義中的生命週期詮釋

目前為止,我們定義過的結構體都持有型別的所有權。結構體其實也能持有參考,不過我們會需要在結構體定義中每個參考加上生命週期詮釋。範例 10-24 有個持有字串切片的結構體 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("無法找到 '.'");
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

範例 10-24:擁有參考的結構體需要加上生命週期詮釋

此結構體有個欄位 part 並擁有字串切片參考。如同泛型資料型別,我們在結構體名稱之後的尖括號內宣告泛型生命週期參數,所以我們就可以在結構體定義的本體中使用生命週期參數。此詮釋代表 ImportantExcerpt 的實例不能比它持有的欄位 part 活得還久。

main 函式在此產生一個結構體 ImportantExcerpt 的實例並持有一個參考,其為變數 novel 所擁有的 String 中的第一個句子的參考。novel 的資料是在 ImportantExcerpt 實例之前建立的。除此之外,novelImportantExcerpt 離開作用域之前不會離開作用域,所以 ImportantExcerpt 實例中的參考是有效的。

生命週期省略

你已經學到了每個參考都有個生命週期,而且你需要在有使用參考的函式與結構體中指定生命週期參數。然而在第四章的範例 4-9 我們有函式可以不詮釋生命週期並照樣編譯成功,我們在範例 10-25 再展示一次。

檔案名稱: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 能用在`String` 的切片
    let word = first_word(&my_string[..]);

    let my_string_literal = "hello world";

    // first_word 能用在字串字面值
    let word = first_word(&my_string_literal[..]);

    // 因為字串字面值已經是字串切片了
    // 所以也可以不用加上字串語法!
    let word = first_word(my_string_literal);
}

範例 10-25:在範例 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-25 中函式 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-20 一開始沒有任何生命週期參數的 longest 函式:

fn longest(x: &str, y: &str) -> &str {

讓我們先檢查第一項規則:每個參數都有自己的生命週期。這次我們有兩個參數,所以我們有兩個生命週期:

fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {

你可以看出來第二個規則並不適用,因為我們有不止一個輸入生命週期。而第三個也不適用,因為 longest 是函式而非方法,其參數不會有 self 。遍歷這三個規則下來,我們仍然無法推斷出回傳型別的生命週期。這就是為何我們嘗試編譯範例 10-20 的程式碼會出錯的原因:編譯器遍歷生命週期省略規則,但仍然無法推導出簽名中所有參考的生命週期。

因為第三個規則僅適用於方法簽名,我們接下來就會看看這種情況時的生命週期,看看為何第三個規則讓我們不必常常在方法簽名詮釋生命週期。

在方法定義中的生命週期詮釋

當我們在有生命週期的結構體上實作方法時,其語法類似於我們在範例 10-11 中泛型型別參數的語法。宣告並使用生命週期參數的地方會依據它們是否與結構體欄位或方法參數與回傳值相關。

結構體欄位的生命週期永遠需要宣告在 impl 關鍵字後方以及結構體名稱後方,因為這些生命週期是結構體型別的一部分。

impl 區塊中方法簽名的參考可能會與結構體欄位的參考生命週期綁定,或者它們可能是互相獨立的。除此之外,生命週期省略規則常常可以省略方法簽名中的生命週期詮釋。讓我們看看範例 10-24 定義過的 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 用第一個生命週期省略規則給予 &selfannouncement 它們自己的生命週期。然後因為其中一個參數是 &self,回傳型別會取得 &self 的生命週期,如此一來所有的生命週期都推導出來了。

靜態生命週期

其中有個特殊的生命週期 'static 我們需要進一步討論,這是指該參考可以存活在整個程式期間。所有的字串字面值都有 'static 生命週期,我們可以這樣詮釋:

#![allow(unused)]
fn main() {
let s: &'static str = "我有靜態生命週期。";
}

此字串的文字會直接儲存在程式的執行檔中,所以永遠有效。因此所有的字串字面值的生命週期都是 'static

你有時可能會看到錯誤訊息建議使用 '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-21 會回傳兩個字串切片較長者的 longest 函式。不過現在它有個額外的參數 ann,使用的是泛型型別 T,它可以是任何在 where 中所指定有實作 Display 特徵的型別。此額外參數會在 {} 的地方印出來,這正是為何 Display 的特徵界限是必須的。因為生命週期也是一種泛型,生命週期參數 'a 與泛型型別參數 T 都宣告在函式名稱後的尖括號內。

總結

我們在此章節涵蓋了許多內容!現在你已經知道泛型型別參數、特徵與特徵界限以及泛型生命週期參數,你已經準備好能寫出適用於許多不同情況且不重複的程式碼了。泛型型別參數讓你可以讓程式碼適用於不同型別;特徵與特徵界限確保就算型別為泛型,它們都會有相同的行為。你還學到了使用生命週期詮釋確保此如此彈性的程式碼不會造成迷途參考。而且這些分析都發生在編譯期間,完全不影響執行的效能!

不管你信不信,本章節還有很多延伸主題可以導論,像是第十七章就會討論特徵物件(trait objects),這是另一個使用特徵的方法。另外還有一些更複雜的場合會涉及到更進階的生命週期詮釋。對此你可能就會想閱讀 Rust Reference。接下來,你將學習如何在 Rust 寫測試,讓你可以確保程式碼能如期執行。