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還有幾個深坑,我們下次繼續。