Rust 生命週期

簡介

生命週期對於許多 Rust 的初學者來說是一個很難理解的概念。它是一個新的概念,以至於大多數程序員從未在其他任何語言中見過它。在本文中,我們剖析生命週期的意義,並提供清晰識別生命週期的方法。

生命週期的目的

在討論細節之前,讓我們首先理解爲什麼存在生命週期,它們的作用是什麼?生命週期可以幫助編譯器執行一個簡單的規則:任何引用都不應該比它的值活得長。換句話說,生命週期幫助編譯器消除懸垂指針錯誤。編譯器通過分析所涉及的變量的生命週期來實現這一點。如果引用的生命週期小於值的生命週期,代碼就會編譯,否則就不會編譯。

“生命週期” 這個詞的含義

生命週期如此令人困惑的部分原因在於,在 Rust 的大部分寫作中,生命週期這個詞被輕率地用於指代三種不同的東西——變量的實際生命週期、生命週期約束條件和生命週期註釋。讓我們一個一個來看。

變量的生命週期

這是簡單的,變量的生命週期是指它存活的時間。這個意思最接近詞典中關於事物存在一段時間的含義。例如,在下面的代碼中,x 的生命週期會一直延伸到外部塊的末尾,而 y 的生命週期會在內部塊的末尾結束。

{
    let x: Vec<i32> = Vec::new();//---------------------+
    {//                                                 |
        let y = String::from("Why");//---+              | x's lifetime
        //                               | y's lifetime |
    }// <--------------------------------+              |
}// <---------------------------------------------------+

生命週期約束條件

變量在代碼中的交互方式對它們的生命週期施加了一些約束。例如,在下面的代碼中,添加一個約束,x 的生命週期應該包含在 y 的生命週期內:

//error:`y` does not live long enough
{
    let x: &Vec<i32>;
    {
        let y = Vec::new();//----+
//                               | y's lifetime
//                               |
        x = &y;//----------------|--------------+
//                               |              |
    }// <------------------------+              | x's lifetime
    println!("x's length is {}", x.len());//    |
}// <-------------------------------------------+

如果沒有添加這個約束,println! 可以訪問 x,x 是 y 的引用,而 y 在上一行中會被銷燬。

請注意,約束不會改變實際的生命週期—例如,x 的生命週期仍然擴展到外部塊的末尾—它們只是編譯器用來禁止懸垂引用的工具。在上面的例子中,實際的生命週期不滿足約束條件:x 的生命週期已經超出了 y 的生命週期。因此,這段代碼無法編譯。

生命週期註釋

很多時候編譯器自動生成生命週期約束,但是隨着代碼變得更加複雜,編譯器會要求程序員手動添加約束,程序員通過生命週期註釋來實現這一點。例如,在下面的代碼片段中,編譯器需要知道 print_ret 函數返回的引用是否借用了 s1 或 s2,因此編譯器要求程序員顯式地添加這個約束:

//error:missing lifetime specifier
//this function'return type contains a borrowed value,
//but the signature does not say whether it is borrowed from `s1` or `s2`
fn print_ret(s1: &str, s2: &str) -> &str {
    println!("s1 is {}", s1);
    s2
}

fn main() {
    let some_str: String = "Some string".to_string();
    let other_str: String = "Other string".to_string();
    let s1 = print_ret(&some_str, &other_str);
}

程序員需要用'a 註釋 s2 和返回的引用,從而告訴編譯器返回值是從 s2 借來的:

fn print_ret<'a>(
    s1: &str, 
    s2: &'a str
) -> &'a str {
    println!("s1 is {}", s1);
    s2
}

fn main() {
    let some_str: String = "Some string".to_string();
    let other_str: String = "Other string".to_string();
    let s1 = print_ret(&some_str, &other_str);
}

我想強調的是,僅僅因爲註釋'a 出現在參數 s2 和返回的引用上,並不意味着 s2 和返回的引用具有完全相同的生命週期。相反,這應該讀作:返回的帶有註釋'a 的引用是從具有相同註釋的實參借來的。

由於 s2 進一步借用了 other_str,生命週期的約束是:返回的引用不能比 other_str 長。代碼編譯通過是因爲確實滿足了生命週期約束:

fn print_ret<'a>(
    s1: &str, 
    s2: &'a str
) -> &'a str {
    println!("s1 is {}", s1);
    s2
}

fn main() {
    let some_str: String = "Some string".to_string();
    let other_str: String = "Other string".to_string();//-------------+
    let ret = print_ret(&some_str, &other_str);//---+                 | other_str's lifetime
    //                                              | ret's lifetime  |
}// <-----------------------------------------------+-----------------+

在展示更多示例之前,讓我簡要介紹一下生命週期註釋語法。要創建生命週期註釋,必須首先聲明生命週期參數。例如,<'a> 是生命週期聲明。生命週期參數是一種泛型參數,一旦聲明瞭生命週期參數,就可以在引用中使用它來創建生命週期約束。

記住,通過使用'a 註釋引用,程序員只是制定了一些約束;然後,編譯器的工作就是爲'a 找到滿足約束的具體生命週期引用。

例子

接下來,考慮一個函數 min,它找到兩個值的最小值:

fn min<'a>(
    x: &'a i32, 
    y: &'a i32
) -> &'a i32 {
    if x < y {
        x
    } else {
        y
    }
}

fn main() {
    let p = 42;
    {
        let q = 10;
        let r = min(&p, &q);
        println!("Min is {}", r);
    }
}

在這裏,'a 形參註釋了參數 x、y 和返回值。這意味着返回值可以從 x 或 y 中借用。由於 x 和 y 分別從 p 和 q 中借用,返回的引用的生命週期也應該包含在 p 和 q 的生命週期中。這段代碼可以編譯通過,因爲滿足了約束條件:

fn min<'a>(
    x: &'a i32, 
    y: &'a i32
) -> &'a i32 {
    if x < y {
        x
    } else {
        y
    }
}

fn main() {
    let p = 42;//-------------------------------------------------+
    {//                                                           |
        let q = 10;//------------------------------+              | p's lifetime
        let r = min(&p, &q);//------+              | q's lifetime |
        println!("Min is {}", r);// | r's lifetime |              |
    }// <---------------------------+--------------+              |
}// <-------------------------------------------------------------+

通常,當函數有兩個或多個引用參數時,返回的引用生命週期不能超過生命週期最短的引用參數。

最後一個例子,許多新的 c++ 程序員都會犯返回局部變量指針的錯誤。在 Rust 中,類似的嘗試是不允許的:

//Error:cannot return reference to local variable `i`
fn get_int_ref<'a>() -> &'a i32 {
    let i: i32 = 42;
    &i
}

fn main() {
    let j = get_int_ref();
}

由於 get_int_ref 函數沒有實參,編譯器知道返回的引用必須借用局部變量,這是不允許的。編譯器正確地避免了災難,因爲當返回的引用試圖訪問局部變量時,它將被清除:

fn get_int_ref<'a>() -> &'a i32 {
    let i: i32 = 42;//-------+
    &i//                     | i's lifetime
}// <------------------------+

fn main() {
    let j = get_int_ref();//-----+
//                               | j's lifetime
}// <----------------------------+

消除規則

當編譯器允許程序員省略生命週期註釋時,稱爲生命週期省略。再說一遍,“生命週期省略” 這個術語是有誤導性的——當生命週期與變量的存在和消失不可分割地聯繫在一起時,它怎麼可能被省略呢?被省略的不是生命週期,而是生命週期註釋,以及擴展生命週期約束。在早期版本的 Rust 編譯器中,不允許省略,並且需要每個生命週期註釋。但是隨着時間的推移,編譯器團隊觀察到生命週期註釋的相同模式不斷重複。所以制定了消除規則。

在以下情況下,程序員可以省略註釋:

當只有一個輸入引用時。在這種情況下,將輸入的生命週期註釋分配給所有輸出引用。

例如:

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

當有多個輸入引用時,但第一個參數是 & self 或 & mut self。在這種情況下,第一個參數的生命週期註釋也被分配給所有輸出引用。

例如:

fn some_method(&self) -> &str
fn some_method<'a>(&'a self) -> &'a str

總結

變量的生命週期必須滿足編譯器和程序員對它們施加的某些約束,然後編譯器才能確保代碼是安全的。如果沒有生命週期機制,編譯器將無法保證大多數 Rust 程序的安全性。

本文翻譯自:

https://hashrust.com/blog/lifetimes-in-rust/


coding 到燈火闌珊 專注於技術分享,包括 Rust、Golang、分佈式架構、雲原生等。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/qyCWevEM8zgVxVv13c0nIA