【Rust學習筆記】Rust生命週期參數的詳細闡述

【Rust學習筆記】Rust生命週期參數的詳細闡述
原創 acodercat Rust語言中文社區 前天
https://mp.weixin.qq.com/s/XpX-BN__R9ylLZVjmEnx5g
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 `

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

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行定義了一個靜態變量,它的生命週期是’static 。max_num函數的生命週期參數’a會取變量x的生命週期和變量Y的生命週期重疊的部分。所以傳入max_num函數並不會報錯。
總結
以上內容是我個人在學習rust生命週期參數相關內容時的總結,如有錯誤歡迎指正。文中的借用和引用實際上是一個東西。

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