Rust 學習筆記:生命週期

2023年10月22日 · 中文


前言

本系列文書寫我個人初探 Rust 的學習筆記,章節劃分主要基於著名的 The BookThe 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 會使用 s1s2 兩者中生命週期較短的一個做為 '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
}

若把 s1s2 改成 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
}