Rust入坑指南:朝生暮死

今天想和大家一起把我們之前挖的坑再刨深一些。在Java中,一個對象能存活多久全靠JVM來決定,程序員並不需要去關心對象的生命週期,但是在Rust中就大不相同,一個對象從生到死我們都需要掌握的很清楚。

Rust入坑指南:核心概念一文中我們介紹了Rust的幾個核心概念:所有權(Ownership)、所有權轉移和所有權借用。今天就來介紹Rust中的另外一個核心概念:生命週期。

爲什麼生命週期要單獨介紹呢?因爲我在這之前一直沒搞清楚Rust中的生命週期參數究竟是怎麼一回事。

現在我終於弄明白了,於是迫不及待要和大家分享,當然如果我有什麼說的不對的地方請幫忙指正。

在Rust中,值的生命週期與作用域有關,這裏你可以結合所有權一起理解。在一個函數內,Rust中值的所有權的範圍即爲其生命週期。Rust通過借用檢查器對值的生命週期進行檢查,其目的是爲了避免出現懸垂指針。這點很容易理解,我們通過一段簡單的代碼來看一下。

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

在上面這段代碼中,我已經標註了a和b的生命週期。在代碼的第5行,b將所有權出借給了a,而在第7行我們想使用a時,b的生命週期已經結束,也就是說,從第7行開始,a成爲了一個懸垂指針。因此這段代碼會報一個編譯錯誤。

在這裏插入圖片描述

而當所有權在函數之間傳遞時,Rust的借用檢查器就沒有辦法來確定值的生命週期了。這個時候我們就需要藉助生命週期參數來幫助Rust的借用檢查器來進行生命週期的檢查。生命週期參數分爲顯式的和隱式的兩種。

顯式生命週期參數

顯式生命週期的標註方式通常是'a這樣的。它應該寫在&之後,mut之前(如果有)。

函數簽名中的生命週期參數

在正式開始學習之前,我們還要先明確一些概念。下面是一個代有生命週期參數的函數簽名。

fn foo <'a>(s: &'a str, t: &'a str) ->&'a str;

其中第一個'a,是生命週期參數的聲明。參數的生命週期叫做輸入聲明週期,返回值的生命週期叫做輸出生命週期。需要記住的一點是:輸出的生命週期長度不能長於輸入的生命週期

另外還要注意:禁止在沒有任何輸入參數的情況下返回引用。因爲這樣明顯會造成懸垂指針。試想當你沒有任何輸入參數時返回了引用,那麼引用本身的值在函數返回時必然會被析構,返回的引用也就成了懸垂指針。

同樣的道理我們可以得出另一個結論:從函數中返回一個引用,其生命週期參數必須與函數的參數相匹配,否則,標註生命週期參數也毫無意義

說了這麼多“不允許”之後,我們來看一個正常使用生命週期參數的例子吧。

fn the_longest<'a> (s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}
fn main() {
    let s1 = String::from("Rust");
    let s1_r = &s1;
    {
        let s2 = String::from("C");
        let res = the_longest(s1_r, &s2);
        println!("{} is the longest", res);
    }
}

我們來看看這段代碼的各個值的生命週期是否符合我們前面說的那一點原則。在調用th_longest函數時,兩個參數的生命週期已經確定,s1的生命週期貫穿了main函數,s2的生命週期在內部的代碼塊中。函數返回時,將返回值綁定給了res,也就是說返回的生命週期爲res的生命週期,由於後定義先析構的原則,res的生命週期是短於s2的生命週期的,當然也短於s1的生命週期。因此這個例子符合了我們說的輸出的生命週期長度不能長於輸入的生命週期的原則。

對於像示例當中有多個參數的函數,我們也可以爲其標註不同的生命週期參數,但是編譯器無法確定兩個生命週期參數的大小,因此需要我們顯式的指定。

fn the_longest<'a, 'b: 'a> (s1: &'a str, s2: &'b str) -> &'a str {
    if s1.len() > s2.len() {
        s1
    } else {
        s2
    }
}

這裏'b: 'a的意思是'b的存活週期長於'a。這點有些令人疑惑,'a明明是長於'b的,爲什麼會這樣標註呢?還記得我們說過生命週期參數的意義嗎?它是用來幫助Rust借用檢查器來檢查非法借用的,輸出生命週期必須短於輸入生命週期。因此這裏的'a實際上是返回值的生命週期,而不是第一個輸入參數的生命週期。

函數中的生命週期參數的使用我們暫時先介紹到這裏。生命週期在其他使用場景中的使用方法也比較類似,不過還是有一些值得注意的地方的。

結構體中的生命週期參數

如果一個結構體包含引用類型的成員,那麼結構體應該聲明生命週期參數<'a>。這是爲了保證結構體實例的生命週期應該短於或等於任意一個成員的生命週期

struct ImportantExcept<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("call me Ishmael. Some year ago...");
    let first_sentence = novel.split('.')
        .next()
        .expect("Could not find a '.'");
    let i = ImportantExcept { part: first_sentence};
    assert_eq!(i.part, "call me Ishmael");
}

在這段代碼中first_sentence先於結構體實例i被定義,因此i的生命週期是短於first_sentence的,如果反過來,i的生命週期長於first_sentence即長於part,那麼在part被析構以後,i.part就會成爲懸垂指針。

方法中的生命週期參數

現在我們爲剛纔的結構體增加一個實現方法

impl<'a> ImportantExcept<'a> {
    fn get_first_sentence(s: &'a str) -> &'a str {
        let first_sentence = s.split('.')
            .next()
            .expect("Could not find a '.'");
        first_sentence
    }
}

因爲ImportantExcept包含引用成員,因此需要標註生命週期參數。在impl後面聲明生命週期參數<'a>在結構體名稱後面使用。在get_first_sentence方法中使用的生命週期參數也是剛剛定義好的那個。這樣就可以約束輸入引用的生命週期長度長於結構體實例的生命週期長度。

靜態生命週期參數

前面聊的都是我們自己定義的生命週期參數,現在來聊聊Rust中內置的生命週期參數'static'static生命週期存活於整個程序運行期間。所有的字符串字面量都有'static生命週期,類型爲&'static str

隱式生命週期參數

在某些情況下,我們可以省略生命週期參數,對於省略的生命週期參數通常有三條規則:

  • 每個輸入位置上省略的生命週期都將成爲一個不同的生命週期參數
  • 如果只有一個輸入生命週期的位置,則該生命週期將分配給輸出生命週期
  • 如果存在多個輸入生命週期的位置,但是其中包含&self或&mut self,則self的生命週期將分配給輸出生命週期

生命週期限定

生命週期參數也可以像trait那樣作爲範型的限定

  • T: 'a:表示T類型中的任何引用都要“活得”和’a一樣長
  • T:Trait + 'a:表示T類型必須實現Trait這個trait,並且T類型中的任何引用都要“活得”和’a一樣長

總結

現在我把我對Rust生命週期的瞭解都分享完了。其實只要記住一個原則就可以了,那就是:生命週期參數的目的是幫助借用檢查器驗證引用的合法性,避免出現懸垂指針

Rust還有幾個深坑,我們下次繼續。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章