2020/04/20

rust10 Generic Types, Traits, and Lifetimes

每個程式語言都有處理重複概念的工具,rust 的工具之一是 generic types,泛型是具體類別或其他屬性的抽象替代。撰寫程式碼,就是表達 generics 的行為,或是如何跟其他 generics 互動,不需要知道在寫程式或是編譯時,實際上代表什麼東西。

首先回顧提取函數減少重複程式碼的機制。然後使用一個只在參數類別上不同的泛型函數,來實現相同的功能,另外也會討論 struct 及 enum 的泛型。

然後討論 trait,這是定義泛型行為的方法。 trait 可跟泛型結合,將泛型限制為有特定行為的類別。

最後是 lifetimes,是允許向 compiler 提供引用如何相互關聯的泛型。rust 的 lifetime 可在很多 borrow values 狀況下,還能讓 compiler 檢查 references 是否 valid。

提取函數

提取函數可不使用 generic types 處理重複程式碼的問題。

// 在整數 vector 中,尋找最大值的函數
fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let mut largest = number_list[0];

    // iterate 所有整數,將最大值存放到 largest
    for number in number_list {
        if number > largest {
            largest = number;
        }
    }

    println!("The largest number is {}", largest);
}

如果還有另一個 vector,會發生重複 code 的問題,可提取 largest function

fn largest(list: &[i32]) -> i32 {
    let mut largest = list[0];

    for &item in list.iter() {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {}", result);

    let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];

    let result = largest(&number_list);
    println!("The largest number is {}", result);
}

步驟:

  1. 找出重複 code
  2. 將重複的 code 提到一個函數中,在函數定義上,指定輸入及返回值
  3. 重複 code 改為呼叫函數

Generic Data Types

瞭解如何用泛型定義 function, structs, enum, method

在函數定義中使用泛型

這是兩種不同的參數類別的 largest function

fn largest_i32(list: &[i32]) -> i32 {
    let mut largest = list[0];

    for &item in list.iter() {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn largest_char(list: &[char]) -> char {
    let mut largest = list[0];

    for &item in list.iter() {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest_i32(&number_list);
    println!("The largest number is {}", result);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest_char(&char_list);
    println!("The largest char is {}", result);
}

任何 id 都可以當作 type parameter name,通常習慣會使用 T

// 泛型的參數宣告,必須放在函數名稱後面,參數前面的 <> 裡面
// 這個函數有一個參數 list,類別為 T 的 slice
// 會回傳 T
fn largest<T>(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list.iter() {
        // 會發生編譯錯誤
        // error[E0369]: binary operation `>` cannot be applied to type `T`
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {}", result);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest(&char_list);
    println!("The largest char is {}", result);
}

編譯錯誤

error[E0369]: binary operation `>` cannot be applied to type `T`
 --> src/main.rs:8:12
  |
8 |         if item > largest {
  |            ^^^^^^^^^^^^^^
  |
  = note: `T` might need a bound for `std::cmp::PartialOrd`

error: aborting due to previous error

std::cmp::PartialOrd是一個 trait,這表示 largest 實際上不適用所有類別,因為函數中需要比較 T 類別的值。標準庫中定義的 std::cmp::PartialOrd trait 可以實現類別的比較功能,在後面的章節討論。

structs 定義中的泛型

可使用 <> 定義一個或多個泛型參數的 structs

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let integer = Point { x: 5, y: 10 };
    let float = Point { x: 1.0, y: 4.0 };
}

在 struct 名稱後面用 <T> 宣告泛型參數的名稱,然後就可在 struct 中使用 T

因為 x, y 使用了相同的泛型宣告 T,因此 x, y 如果是不同的類別,就會無法編譯

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    // 編譯錯誤
    let wont_work = Point { x: 5, y: 4.0 };
}

改用不同的泛型宣告

struct Point<T, U> {
    x: T,
    y: U,
}

fn main() {
    let both_integer = Point { x: 5, y: 10 };
    let both_float = Point { x: 1.0, y: 4.0 };
    let integer_and_float = Point { x: 5, y: 4.0 };
}

enum 定義中的泛型

enum Option<T> {
    Some(T),
    None,
}

enum Result<T, E> {
    Ok(T),
    Err(E),
}

method 定義中的泛型

struct Point<T> {
    x: T,
    y: T,
}

// 實作 method x,會回傳 T 類別的 x 的 reference
// 必須在 impl 後面加上泛型宣告 <T>,這樣 compiler 才知道這是泛型,就可以用在 Point<T>
impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

// 僅針對 T 是 f32 的狀況,提供一個 method
impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());

    let p2 = Point { x: 5.0, y: 10.0 };
    println!("distance_from_origin={}", p2.distance_from_origin());

    let p3 = Point { x: 5, y: 10 };
    // error[E0599]: no method named `distance_from_origin` found for type `Point<{integer}>` in the current scope
    //println!("distance_from_origin={}", p3.distance_from_origin());
}

struct Point<T, U> {
    x: T,
    y: U,
}

// 這個泛型宣告是針對 struct 定義
impl<T, U> Point<T, U> {
    // mixup 另一個 Point,且其泛型宣告跟上面的宣告不同
    // 也就是參數裡面的 Point 裡面的型別,跟呼叫這個 method 的型別不同
    // 這邊的泛型宣告是針對 method 裡面的參數,回傳的泛型宣告,就混合了兩個泛型定義
    fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 5, y: 10.4 };
    let p2 = Point { x: "Hello", y: 'c'};

    let p3 = p1.mixup(p2);

    println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
    // p3.x = 5, p3.y = c
}

使用泛型程式碼的效能

使用泛型不會影響效能

rust 在編譯時,會將泛型程式碼進行 monomorphization 單態化,也就是填充編譯時使用的具體類別,將通用程式碼轉換為特定程式碼的過程。編譯器會尋找所有泛型程式碼被呼叫的位置,並使用泛型程式碼針對具體類型生成binary。

let integer = Some(5);
let float = Some(5.0);

rust 在編譯時會進行 monomorphization,發現傳給 Option 的值有兩種,一個是 i32,一個是 f64,然後會將 Option<T> 展開變成 Option_i32Option_f64,然後將泛型定義替換為這兩個具體的定義。

enum Option_i32 {
    Some(i32),
    None,
}

enum Option_f64 {
    Some(f64),
    None,
}

fn main() {
    let integer = Option_i32::Some(5);
    let float = Option_f64::Some(5.0);
}

Traits: 定義分享的行為

trait 是告訴 compiler 某些類別共享的行為,另外 trait bounds 指定泛型是有某些特定行為的類別。

trait 類似其他程式語言中的 interface,但有一些不同。

定義 trait

一個類別的行為由其提供呼叫的 method 組成。如果可對不同類別呼叫相同的 method,這些類別就共享了相同的行為。

trait 是一種將 method signatures 組合起來的方法,目的是定義一個實現某種功能所需的所有行為的集合。

ex: 有存放多種不同長度的文字 structs: (1) NewsArticle struct 儲存在某特定地區的 news story (2) Tweet 存 280 chars 的文字,有 meta 代表這是 new tweet/retweet/reply

現在想做一個 aggregator library,顯示所有資料的 summary。我們需要對每一個類別都提供 summary method。這是 summary trait 的定義:

pub trait Summary {
    fn summarize(&self) -> String;
}

compiler 會檢查,所有包含 Summary trait 的類別,都實作了 summarize method 的 method body。

實作 trait

剛剛已經定義了 Summary trait,現在要在 NewsArticle, Tweet 都實作 summarize

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

在類別實作 trait 就跟一般的 method 一樣,差別是 impl 後面,要提供 trait 的名稱。

現在就可以呼叫 summarize

let tweet = Tweet {
    username: String::from("horse_ebooks"),
    content: String::from("of course, as you probably already know, people"),
    reply: false,
    retweet: false,
};

println!("1 new tweet: {}", tweet.summarize());

如果 Summary trait 放在另一個 aggregator crate 裡面,必須將 trait 引入 scope,也就是 use aggregator:Summary; ,另外 Summary 必須是 pub,讓其他 crate 可以使用。

實作 trait 要注意,只有 trait 或實作 trait 的類別位於 crate 的本地 scope 中,才能為該類別實作 trait。例如可對 Tweet 實作 std lib 裡面的 Display trait,因為 Summary trait 位於 lib crate 裡面,所以可以在 Tweet 實作 Summary。

無法對外部類別實作外部 trait,例如不能在 lib crate 為 Vec<T> 實作 Display trait,這是因為 DisplayVec<T> 都定義在 std lib 裡面。這個限制稱為: coherence,也稱為 orpan rule。可確保其他人的程式碼,不會破壞你的 code。沒有這個規則時,兩個 crate 可同時對相同的類別實作相同的 trait,compiler 會無法判斷該使用哪一個實作。

Default Implementation

有時候可為某些 method 提供預設的行為。在類別中實作 trait 時,可自行決定要不要 override 預設行為。

以下定義了 Summary trait 同時提供預設實作。

pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

以下是 NewsArticle 使用預設的 Summary

impl Summary for NewsArticle {
}

traits as parameters

瞭解如何使用 trait 來接受不同類別的參數。

先前已經定義了 NewsArticle, Tweet 並實作 Summary trait。可再定義一個函數 notify 呼叫參數 itemsummarize method,該參數是實作了Summary trait 的某種類別。

pub fn notify(item: impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

notify 函數可以呼叫任何來自 Summary trait 的方法,比如 summarize

trait bound syntax

impl Trait 語法適用於精簡的例子,實際上完整的 trait bound 應該是

pub fn notify<T: Summary>(item: T) {
    println!("Breaking news! {}", item.summarize());
}

trait bound 跟泛型參數宣告放在一起,放在 <> 後面,因為 T 的 trait bound,我們可傳入 NewsArticle or Tweet 的 instance,並呼叫 notify。

trait bound 適合複雜的場景,例如需要獲取兩個實作 Summary 的不同類別

pub fn notify(item1: impl Summary, item2: impl Summary) {

這是獲取兩個實作 Summary 的相同類別,要用 trait bound

pub fn notify<T: Summary>(item1: T, item2: T) {

利用 + 語法,指定多個 trait bounds

如果 notify 需要顯示 item 的格式化形式,同時要使用 summarize method,那麼 item 就需要實作兩個不同的 trait: Display and Summary

pub fn notify(item: impl Summary + Display) {

也適用 trait bound

pub fn notify<T: Summary + Display>(item: T) {

where 簡化程式碼

使用太多 trait bound 也有缺點,每個泛型有自己的 trait bound,所以有多個泛型參數的函數,在名稱與參數列表之間,會有很長的 trait bound,這會讓 method 很難閱讀。因此可改用 where 指定 trait bound

fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {

改用 where

fn some_function<T, U>(t: T, u: U) -> i32
    where T: Display + Clone,
          U: Clone + Debug
{

實作 traits 的回傳值

在回傳值的地方,用 impl Summary 語法

fn returns_summarizable() -> impl Summary {
    Tweet {
        username: String::from("horse_ebooks"),
        content: String::from("of course, as you probably already know, people"),
        reply: false,
        retweet: false,
    }
}

這表示要回傳某個實現 Summary trait 的類別,但不確定是哪一個類別。實際上在實作中是回傳 Tweet

在 chap13 會介紹 clousures, iterators 這兩個依賴 trait 的功能。

但目前這樣的實作方式,只適合用在回傳單一種類別的情況,以下的程式碼無法編譯。因為會回傳 NewsArticle or Tweet,但 impl Trait 的實作,不接受這種程式碼。

fn returns_summarizable(switch: bool) -> impl Summary {
    if switch {
        NewsArticle {
            headline: String::from("Penguins win the Stanley Cup Championship!"),
            location: String::from("Pittsburgh, PA, USA"),
            author: String::from("Iceburgh"),
            content: String::from("The Pittsburgh Penguins once again are the best
            hockey team in the NHL."),
        }
    } else {
        Tweet {
            username: String::from("horse_ebooks"),
            content: String::from("of course, as you probably already know, people"),
            reply: false,
            retweet: false,
        }
    }
}

用 trait bounds 修正 largest

剛剛有問題的程式碼:

// 泛型的參數宣告,必須放在函數名稱後面,參數前面的 <> 裡面
// 這個函數有一個參數 list,類別為 T 的 slice
// 會回傳 T
fn largest<T>(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list.iter() {
        // 會發生編譯錯誤
        // error[E0369]: binary operation `>` cannot be applied to type `T`
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {}", result);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest(&char_list);
    println!("The largest char is {}", result);
}

因 largest 要用 > 比較兩個 T 類別的值,> 是定義在 std::cmp::PartialOrd ,需要在 T 的 trait bound 中指定 PartialOrd,讓 largest 可用在任何可比較大小的類別的 slice。修改 largest 的定義

fn largest<T: PartialOrd>(list: &[T]) -> T {

編譯錯誤

error[E0508]: cannot move out of type `[T]`, a non-copy slice
 --> src/main.rs:5:23

這是因為 i32, char 是已知大小,只能存在 stack,因此他們實現了 Copy trait,當把 largest 改為泛型,list 的參數類別有可能沒有實作 Copy trait

fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list.iter() {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {}", result);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest(&char_list);
    println!("The largest char is {}", result);
}

fn largest<T: PartialOrd + Copy>(list: &[T]) -> T { largest 要增加 PartialOrd + Copy trait

也可以將 Copy 換成 Clone trait,clone 就代表可處理類似String 這種在 heap 的資料,但可能會因為大量資料造成速度變慢。

另一種 largest 實作方法是回傳 slice 中 T 的引用,改為 &T,這樣就不需要用 Copy/Clone trait bounds

使用 trait bound 有條件地實作方法

Pair<T> 實作了 new,但只有為 T 實作了 PartialOrd 與 Display trait 的 Pair<T> 才會實作 cmp_display method

use std::fmt::Display;

struct Pair<T> {
    x: T,
    y: T,
}

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self {
            x,
            y,
        }
    }
}

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
}

對任何滿足特定 trait bound 的類別實作 trait 也稱為 blanket implementations,常用在 rust std lib。例如 std lib 為 Display trait 實作 ToString trait

impl<T: Display> ToString for T {
    // --snip--
}

因此可對任何實作 Display trait 的類別呼叫 ToString 的 to_string

blanket implementation 會出現在 trait 文件的 "Implementers" 部分。

trait 及 trait bound 讓我們使用泛型減少重複 code,且能向 compiler 明確指定類別行為。因為 trait bound 讓 compiler 能檢查類別是否提供正確的行為。

rust 將錯誤由執行期移動到編譯期。

lifetimes 是另一種泛型,可確保引用類別時,一直有效

以 Lifetimes 驗證 references 有效性

rust 每一個引用都有 lifetime,也就是引用有效的 scope。rust 需要用泛型 lifetime 參數註明 lifetime 的關係,確保引用永遠有效。

lifetime 是這個語言最特別的功能

Preventing Dangling References with Lifetimes

dangling reference: 引用了非預期引用的資料。


{
        // 宣告沒有初始值的變數,存在於 outer scope
    let r;

    {
            // 在 inner scope 將 r 設定為 inner 變數 x 的 reference
        let x = 5;
        // 編譯錯誤:error[E0597]: `x` does not live long enough
        r = &x;
    }

    // 列印 r
    println!("r: {}", r);
}

rust 是透過 borrow checker 檢查這段程式碼

borrow checker

r 的 lifetime 為 'a,x 為 'b, 'b 明顯比 'a 小

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

這是有效的引用,因為資料比引用有更長的 lifetime

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

函數中的泛型生命週期

一個返回兩個字符串 slice 中較長者的函數。這個函數獲取兩個字符串 slice 並返回一個 String slice。

因為 rust 不知道要回傳的引用是指向 x or y

// 編譯錯誤:error[E0106]: missing lifetime specifier
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

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

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

透過生命週期參數,定義引用之間的關係,讓 borrow checker 能進行分析。

Lifetime Annotation Syntax

lifetime annotation 並不改變任何引用的 lifetime 長短。當指定泛型 lifetime parameter 後,函數也可以接受任何 lifetime 的引用。lifetime annotation 描述多個引用 lifetime 相互的關係,不影響其 lifetime。

lifetime annotation 名稱要以 ' 開頭,名稱通常是小寫,且非常短。預設都是使用 'a 。 lifetime annotation 位於引用的 & 後面,有一個空格將引用類別與生命週期註解分開。

&i32        // 引用
&'a i32     // 帶有顯式生命週期的引用
&'a mut i32 // 帶有顯式生命週期的可變引用

單一個 lifetime annotation 本身沒有意義,因為這是用在多個引用的泛型生命週期參數之間的關係。如果函數有一個 lifetime 'ai32的引用參數 first,還有一個 'ai32 引用的參數 second,就表示 first, second 必須跟這個泛型的 lifetime 一樣久。

Lifetime Annotations in Function Signatures

泛型生命週期參數需要需宣告在函數名稱和參數列表間的<>中間,用意是告訴 Rust 關於參數中的引用和返回值之間的限制是他們都必須擁有相同的生命週期。

以下 longest 的所有的引用必須有相同的生命週期 'a

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

通過在函數簽名中指定生命週期參數時,並沒有改變任何傳入後返回的值的生命週期。而是指出任何不遵守這個協議的傳入值都將被借用檢查器拒絕。longest 函數並不需要知道 xy 具體會存在多久,而只需要知道有某個可以被 'a 替代的作用域將會滿足這個簽名。當函數引用或被函數之外的代碼引用時,讓 Rust 自己分析出參數或返回值的生命週期幾乎是不可能的。生命週期在每次函數被調用時都可能不同。這也就是為什麼我們需要手動標記生命週期。

當具體的引用被傳遞給 longest 時,被 'a 所替代的具體生命週期是 x 的作用域與 y 的作用域相重疊的那一部分。換一種說法就是泛型生命週期 'a 的具體生命週期等同於 xy 的生命週期中較小的那一個。因為我們用相同的生命週期參數 'a 標註了返回的引用值,所以返回的引用值就能保證在 xy 中較短的那個生命週期結束之前保持有效。

如何通過傳遞擁有不同具體生命週期的引用來限制 longest 函數的使用。

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

fn main() {
    // string1 在外部 scope 有效
    let string1 = String::from("long string is long");
    {
        // string2 在內部 scope 有效
        let string2 = String::from("xyz");
        // result 使用內部 scope 有效的 reference
        let result = longest(string1.as_str(), string2.as_str());
        println!("The longest string is {}", result);
    }
}

如果在 string2 離開 scope 後使用 result,會發生編譯錯誤

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        // error[E0597]: `string2` does not live long enough
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {}", result);
}

Thinking in Terms of Lifetimes

如果將 longest 函數的實現修改為總是返回第一個參數而不是最長的字符串 slice,就不需要為參數 y 指定一個生命週期。

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

如果返回的引用 沒有 指向任何一個參數,那麼唯一的可能就是它指向一個函數內部創建的值,它將會是一個 dangling reference。

fn longest<'a>(x: &str, y: &str) -> &'a str {
    // error[E0597]: `result` does not live long enough
    let result = String::from("really long string");
    result.as_str()
}

Lifetime Annotations in Struct Definitions

定義包含引用的 struct,但需要為每一個引用都加上 lifetime annotations

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("Could not find a '.'");
    let i = ImportantExcerpt { part: first_sentence };
}

part,它存放了一個字符串 slice,這是一個引用。

ImportantExcerpt 的實例不能比其 part 字段中的引用存在的更久。

main 函數創建了一個 ImportantExcerpt 的實例,它存放了變數 novel 所擁有的 String 的第一個句子的引用。novel 的數據在 ImportantExcerpt 實例創建之前就存在。另外,直到 ImportantExcerpt 離開作用域之後 novel 都不會離開作用域,所以 ImportantExcerpt 實例中的引用是有效的

Lifetime Elision

每一個引用都有一個生命週期,而且我們需要為那些使用了引用的函數或結構體指定生命週期。

但以下程式,沒有生命週期註解卻能編譯成功:

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[..]
}

這個程式在pre-1.0 的 rust 是不能編譯的,當時必須寫成

fn first_word<'a>(s: &'a str) -> &'a str {

但 rust 團隊發現在特定情況下,可以預測生命週期,因此可讓 borrow checker 自己推測出生命週期。

被編碼進入 rust 引用分析的模式被稱為 lifetime elision rules 生命週期省略規則。這些規則是在某些特定狀況下,編譯器可以自己推測生命週期,不需要明確指定。如果無法推測出生命週期,compiler 會直接給出錯誤,而必須要填寫生命週期註解來解決這個問題。

函數/method 的參數的生命週期稱為 input lifetimes,回傳值的生命週期稱為 output lifetimes。

compiler 採用三條規則,判斷需不需要明確的註解。rule 1 適用於 input lifetime,rule 2, 3 適用於 output lifetimes。這些規則適用於 fn 及 impl 區塊。

rule 1: 每一個是引用的參數,都有自己的生命週期參數。有一個引用參數的函數有一個生命週期參數 ex: fn foo<'a>(x: &'a i32),有兩個引用參數的函數,有兩個生命週期參數 ex: fn foo<'a, 'b>(x: &'a i32, y: &'b i32)

rule 2: 如果只有一個 input lifetime 參數,該 lifetime 會被指定給所有 output lifetime parameters ex: fn foo<'a>(x: &'a i32) -> &'a i32

rule 3: 如果 method 有多個 input lifetime 參數,其中有一個是 &self&mut selfself 的生命週期會被賦予給所有 output lifetime 參數。


fn first_word(s: &str) -> &str {

compiler 套用 rule 1

fn first_word<'a>(s: &'a str) -> &str {

套用 rule 2,因為只有一個 input lifetime 參數,因此所有引用都有了生命週期

fn first_word<'a>(s: &'a str) -> &'a str {

另一個例子

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

compiler 套用 rule 1

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

因有多個input lifetime 參數,不適用 rule 2

因沒有 self 不適用 rule 3

結果還是無法推測出回傳值的 lifetime,因此會出現編譯錯誤

Lifetime Annotations in Method Definitions

在為 struct with lifetime 實作 method 時,使用跟以下的 generic type parameters 相同的語法,宣告及使用 lifetime 參數的位置,跟 struct fields 或 method parameter 與 return values 有關。

struct Point<T, U> {
    x: T,
    y: U,
}

impl<T, U> Point<T, U> {
    fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 5, y: 10.4 };
    let p2 = Point { x: "Hello", y: 'c'};

    let p3 = p1.mixup(p2);

    println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}

struct fields 的 lifetime names 要宣告在 impl 後面,在 struct 名稱的前面,因為 lifetime 是 struct type 的一部分。

impl 區塊裡面的 method signatures,引用會跟 struct 的引用相關或是獨立無關。另外,可套用 lifetime elision rules。


例子

level 方法只有一個 self 參數,且回傳是 i32,不是任何值的引用

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

impl 後面的 lifetime 宣告是必要的,但不需要對 &self 加上 lifetime annotation,因為 elision rule 1


套用 rule 3 的例子

有兩個 input lifetime parameter,套用 rule 1,有兩個 lfietime,因為其中一個是 &self ,套用 rule 3,回傳值被賦予 &self 的 lifetime

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

static lifetime 'static

'static 生命週期存在於整個程式,所有 string literal 都有 'static lifetime

let s: &'static str = "I have a static lifetime.";

這會直接存在程式的 binary code 裡面,讓這個字串永久可以使用

如果錯誤訊息提到了 'static,要先考慮該引用是否是整個程式都有效,才將引用指定為 'static。大部分的情況是遇到 dangling reference,或是無法匹配可用的 lifetimes

Generic Type Parameters, Trait Bounds, and Lifetimes Together

在同一個 fn 裡面同時指定泛型類別參數、trait bounds 和生命週期的語法

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!("Announcement! {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

ann 的類型是泛型 T,可被放入任何實現 where 裡面指定的 Display trait 的類別。這個額外參數,會在函數比較 slice 長度之前被列印出來,這是 Display trai bound 必要的原因。因為 lifetime 也是泛型,所以 'aT 都位於函數名稱後面,同一個 <> 裡面

Summary

泛型類別參數、trait 和 trait bounds 以及泛型生命週期類別

trait 和 trait bounds 保證了即使類別是泛型的,這些類別也會擁有所需要的行為。由生命週期註解所指定的引用生命週期之間的關係保證了這些靈活多變的代碼不會出現懸垂引用。而所有的這一切發生在編譯時所以不會影響運行時效率!

References

The Rust Programming Language

中文版

中文版 2

沒有留言:

張貼留言