細說 rust 生命週期引數

codercat發表於2020-05-17

Rust生命週期

程式中每個變數都有一個固定的作用域,當超出變數的作用域以後,變數就會被銷燬。變數在作用域中從初始化到銷燬的整個過程稱之為生命週期。

rust的每個函式都會有一個作用域,也可以在函式中使用一對花括號再內嵌一個作用域。比如如下程式碼中就在main函式的函式作用域中又內嵌了一個作用域:

fn main() {
  let a;       // --------------+-- a start
  {            //               |
    let b = 5; // -+-- b start  |
  }            // -+-- b over   |
}              // --------------+-- a over

上面程式碼存在兩個作用域,一個是main函式本身的作用域,另外一個是在main函式中使用一對{}定義了一個內部作用域。第2行程式碼宣告瞭變數a,它的作用域是整個main函式,也可以說它的生命週期是從第2行程式碼到第6行程式碼。在第4行程式碼中宣告瞭變數b,它的作用域是第4行到第6行。我們可以發現變數的生命週期是有長短的。

生命週期與借用

rust中的借用是指對一塊記憶體空間的引用。rust有一條借用規則是借用方的生命週期不能比出借方的生命週期還要長。

例如:

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

上面第5行程式碼把變數b借給了變數a,所以a是借用方,b是出借方。可以發現變數a(借用方)的生命週期比變數b(出借方)的生命週期長,於是這樣做違背了rust的借用規則(借用方的生命週期不能比出借方的生命週期還要長)。因為當b在生命週期結束時,a還是保持了對b的借用,就會導致a所指向的那塊記憶體空間已經被釋放了,那麼變數a就會是一個懸垂引用。

執行上面程式碼會報如下錯誤:

error[E0597]: `b` does not live long enough
 --> src/main.rs:5:13
  |
5 |         a = &b;
  |             ^^ borrowed value does not live long enough
6 |     };
  |     - `b` dropped here while still borrowed
7 |     println!("a:{}", a);
  |                      - borrow later used here

意思就是說變數b的生命週期不夠長。變數b已經被銷燬了仍然對它進行了借用。

一個正確的例子:

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

觀察上面程式碼發現變數b(借用方)的生命週期要比變數a(出借方)的生命週期要短,所以借用檢查器會通過。

函式中的生命週期引數

對於一個引數和返回值都包含引用的函式而言,該函式的引數是出借方,函式返回值所繫結到的那個變數就是借用方。所以這種函式也需要滿足借用規則(借用方的生命週期不能比出借方的生命週期還要長)。那麼就需要對函式返回值的生命週期進行標註,告知編譯器函式返回值的生命週期資訊。

我們下面定義一個函式,該函式接收兩個i32的引用型別,返回大的那個數的引用。

示例:

fn max_num(x: &i32, y: &i32) -> &i32 {
  if x > y {
    &x
  } else {
    &y
  }
}

fn main() {
  let x = 1;                // -------------+-- x start
  let max;                  // -------------+-- max start
  {                         //              |
    let y = 8;              // -------------+-- y start
    max = max_num(&x, &y);  //              |
  }                         // -------------+-- y over
  println!("max: {}", max); //              |
}                           // -------------+-- max, x over

由於缺少生命週期引數,編譯器不知道max_num函式返回的引用生命週期是什麼,所以執行報錯:

error[E0106]: missing lifetime specifier
 --> src/main.rs:1:33
  |
1 | fn max_num(x: &i32, y: &i32) -> &i32 {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`

函式的生命週期引數宣告在函式名後的尖括號<>裡,然後每個引數名跟在一個單引號'後面,多個引數用逗號隔開。如果在引數和返回值的地方需要使用生命週期進行標註時,只需要在&符號後面加上一個單引號'和之前宣告的引數名即可。生命週期引數名可以是任意合法的名稱。例如:

fn max_num<'a>(x: &'a i32, y: &'a i32) -> &'a i32 {
  if x > y {
    &x
  } else {
    &y
  }
}
fn main() {
  let x = 1;                // -------------+-- x start
  let max;                  // -------------+-- max start
  {                         //              |
    let y = 8;              // -------------+-- y start
    max = max_num(&x, &y);  //              |
  }                         // -------------+-- y over
  println!("max: {}", max); //              |
}                           // -------------+-- max, x over

上面程式碼對函式max_num的引數和返回值的生命週期進行了標註,用於告訴編譯器函式引數和函式返回值的生命週期一樣長。在第13行程式碼對max_num進行呼叫時,編譯器會把變數x的生命週期和變數y的生命週期與max_num函式的生命週期引數'a建立關聯。這裡值得注意的是,變數x和變數y的生命週期長短其實是不一樣的,那麼關聯到max_num函式的生命週期引數'a的長度是多少呢?實際上編譯器會取變數x的生命週期和變數y的生命週期重疊的部分,也就是取最短的那個變數的生命週期與'a建立關聯。這裡最短的生命週期是變數y,所以'a關聯的生命週期就是變數y的生命週期。

執行上面程式碼,會有報錯資訊:

error[E0597]: `y` does not live long enough
  --> src/main.rs:13:27
   |
13 |         max = max_num(&x, &y);
   |                           ^^ borrowed value does not live long enough
14 |     }
   |     - `y` dropped here while still borrowed
15 |     println!("max: {}", max);
   |                         --- borrow later used here

報錯資訊說變數y的生命週期不夠長,當y的生命週期結束後,仍然被借用。

我們仔細觀察發現max_num函式返回值所繫結到的那個變數max(借用方)的生命週期是從第10行程式碼到第16行程式碼,而max_num函式的返回值(出借方)的生命週期是'a'a的生命週期又是變數x的生命週期和變數y的生命週期中最短的那個,也就是變數y的生命週期。變數y的生命週期是程式碼的第12行到第14行。所以這裡不滿足借用規則(借用方的生命週期不能比出借方的生命週期還要長)。也就是為什麼編譯器會說變數y的生命週期不夠長的原因了。函式的生命週期引數並不會改變生命週期的長短,只是用於編譯來判斷是否滿足借用規則。

將程式碼做如下調整,使其變數max的生命週期小於變數y的生命週期,編譯器就可以正常通過:

fn max_num<'a>(x: &'a i32, y: &'a i32) -> &'a i32 {
  if x > y {
    &x
  } else {
    &y
  }
}
fn main() {
  let x = 1;                  // -------------+-- x start
  let y = 8;                  // -------------+-- y start
  let max = max_num(&x, &y);  // -------------+-- max start
  println!("max: {}", max);   //              |
}                             // -------------+-- max, y, x over

函式存在多個生命週期引數時,需要標註各個引數之間的關係。例如:

fn max_num<'a, 'b: 'a>(x: &'a i32, y: &'b i32) -> &'a i32 {
  if x > y {
    &x
  } else {
    &y
  }
}
fn main() {
  let x = 1;                  // -------------+-- x start
  let y = 8;                  // -------------+-- y start
  let max = max_num(&x, &y);  // -------------+-- max start
  println!("max: {}", max);   //              |
}                             // -------------+-- max, y, x over

上面程式碼使用'b: 'a來標註'a'b之間的生命週期關係,它表示'a的生命週期不能超過'b,即函式返回值的生命週期'a(借用方)不能超過'b``(出借方),‘a也不會超過‘a`(出借方)。

結構體中的生命週期引數

一個包含引用成員的結構體,必須保證結構體本身的生命週期不能超過任何一個引用成員的生命週期。否則就會出現成員已經被銷燬之後,結構體還保持對那個成員的引用就會產生懸垂引用。所以這依舊是rust的借用規則即借用方(結構體本身)的生命週期不能比出借方(結構體中的引用成員)的生命週期還要長。因此就需要在宣告結構體的同時也宣告生命週期引數,同時對結構體的引用成員進行生命週期引數標註。

結構體生命週期引數宣告在結構體名稱後的尖括號<>裡,每個引數名跟在一個單引號'後面,多個引數用逗號隔開。在進行標註時,只需要在引用成員的&符號後面加上一個單引號'和之前宣告的引數名即可。生命週期引數名可以是任意合法的名稱。例如:

struct Foo<'a> {
  v: &'a i32
}

上面程式碼可以把結構體Foo的生命週期與成員v的生命週期建立一個關聯用於編譯器進行借用規則判斷。

函式生命週期引數要注意一點的是,如果函式的引數與函式的返回值不建立生命週期關聯的話,生命週期引數就毫無用處。

下面是一個違反借用規則的例子:

#[derive(Debug)]
struct Foo<'a> {
  v: &'a i32
}

fn main() {
  let foo;                    // -------------+-- foo start
  {                           //              |
    let v = 123;              // -------------+-- v start
    foo = Foo {               //              |
      v: &v                   //              |
    }                         //              |
  }                           // -------------+-- v over
  println!("foo: {:?}", foo); //              |
}                             // -------------+-- foo over

上面程式碼的第14行到15行foo的生命週期依然沒有結束,但是它所引用的變數v已經被銷燬了,因此出現了懸垂引用。編譯器會給出報錯提示:變數v的的生命週期不夠長。

靜態生命週期引數

有一個特殊的生命週期引數叫static,它的生命週期是整個應用程式。跟其他生命週期引數不同的是,它是表示一個具體的生命週期長度,而不是泛指。static生命週期的變數儲存在靜態段中。

所有的字串字面值都是 'static 生命週期,例如:

let s: &'static str = "codercat is a static lifetime.";

上面程式碼中的生命週期引數可以省略,就變成如下形式:

let s: &str = "codercat is a static lifetime.";

還有static變數的生命週期也是'static

例如:

static V: i32 = 123;

下面舉一個特殊的例子:

fn max_num<'a>(x: &'a i32, y: &'a i32) -> &'a i32 {
  if x > y {
    &x
  } else {
    &y
  }
}
fn main() {
  let x = 1;                // -------------+-- x start
  let max;                  // -------------+-- max start
  {                         //              |
    static Y: i32 = 8;      // -------------+-- Y start
    max = max_num(&x, &Y);  //              |
  }                         //              |
  println!("max: {}", max); //              |
}                           // -------------+-- max, Y, x over

還是之前的max_num函式。在程式碼的第12行定義了一個靜態變數,它的生命週期是'staticmax_num函式的生命週期引數'a會取變數x的生命週期和變數Y的生命週期重疊的部分。所以傳入max_num函式並不會報錯。

總結

以上內容是我個人在學習rust生命週期引數相關內容時的總結,如有錯誤歡迎指正。文中的借用和引用實際上是一個東西。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章