iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 6
0
Software Development

30 天深入淺出 Rust系列 第 6

Lifetime: Borrow 的存活時間

Rust 有個重要的功能叫 borrow checker ,它除了檢查在上一篇提到的規則外,還檢查使用者會不會使用到懸空參照 (dangling reference) ,懸空參照是在電腦世界中一種現象: 如果你今天把一個變數借給別人,實際上借走的人只是知道我可以去哪裡找到這個別人借我的東西而已,那個東西的擁有者還是你本人,以現實世界做比喻的話,這像是借別人東西只是把放那個東西的儲物櫃位置,以及鑰匙暫時的交給別人而已,送別人東西則是直接把儲物櫃的擁有者變成他。

所以如果今天發生了一種情況,你把東西借給別人後,管理每個儲物櫃擁有者的系統馬上把你的使用權收回去呢?會發生什麼事,這沒人說的準,可能儲物櫃還沒被清空,你還是可以拿到借來的東西,或是馬上又換了主人,你已經不是拿到原本的東西了,就像以下的程式碼:

fn foo() ->&i32 {
  // 這個變數在離開這個範圍後就消失了
  let a = 42;
  // 但是這邊卻回傳了 borrow
  &a
}

上面這段 code 是無法編譯的。

為了解決這樣的一個問題, Rust 提出來的就是 lifetime 的觀念,只要函式的參數或回傳值有 borrow 出現,使用者就要幫 borrow 標上 lifetime ,標記後讓編譯器可以去追蹤每個變數借出去與釋放掉的情況,確保不會有釋放掉已經出借的變數的可能性。

Rust 使用 'a 一個單引號加上一個識別字當作 lifetime 的標記,所以這些都是可以的 'b, 'foo, '_bar ,此外有兩個保留用作特殊用途的 lifetime: 'static'_

  • 'static: 這代表這是個整個程式都有效的 borrow 比如字串常數 "foo" 它的 lifetime 就是 'static
  • '_:這是保留給 Rust 2018 使用的,這裡先不提它的功能

這邊是個加上 lifetime 標記後的範例:

fn foo<'a>(a: &'a i32) -> &'a i32 {
  a
}

其中我們必須在函式名稱後加上 <> 並在其中宣告我們的 lifetime ,接著把 borrow 的 & 後都加上我們的 lifetime 標記,但事實上在上一篇文章中,我們完全沒用使用到 lifetime , Rust 可以在某些情況下自動推導出正確的 lifetime ,使得實際上需要手動標註的情況並不多,最有可能遇到的情況是一個函式同時使用了兩個 borrow :

fn max<'a>(a: &'a i32, b: &'a i32) -> &'a i32 {
  if a > b {
    a
  } else {
    b
  }
}

fn main() {
  let a = 3;
  let m = &a;
  {
    let b = 2;
    let n = &b;
    // 對於 max 來說, m 與 n 同時存活的這個範圍就是 'a ,
    // 而回傳值也可以在這個範圍內使用
    println!("{}", max(m, n));
  } // b 與 n 會在這邊消失
} // a 與 m 會在這邊消失

這種情況編譯器因為看到了兩個 borrow ,於是沒辦法猜出來回傳的值應該要跟哪個 lifetime 一樣,這邊的作法就是全部都標記一樣的 lifetime ,讓 Rust 知道說我們的變數都會存活在同一個範圍內,同時回傳值也可以在同樣的範圍存活。

大部份的情況下編譯器都能自動的推導,所以需要手動標註的情況其實不多,通常是先嘗試讓編譯器做推導,如果編譯器報錯了才來想辦法標註。

lifetime 還有個用途是用來限制使用者傳入的參數必須是常數:

fn print_message(message: &'static str) {
  println!("{}", message);
}

這個函式就只能接受如 "Hello" 這樣的常數了,雖說只是偶爾會有這樣的需求。

Lifetime Elision (Lifetime 省略規則) (進階)

這部份大概的了解一下就好了

  1. 所有的 borrow 都會自動的分配一個 lifetime
fn foo(a: &i32, b: &i32);
fn foo<'a, 'b>(a: &'a i32, b: &'b i32); // 推導結果
  1. 如果函式只有一個 borrow 的參數,則它的 lifetime 會自動被應用到回傳值上
fn foo(a: &i32);
fn foo<'a>(a: &'a i32) -> &'a i32; // 推導結果
  1. 如果有多個 borrow ,但其中一個是 self ,則 self 的 lifetime 會被應用在回傳值
impl Foo {
  fn method(&self, a: &i32) -> &Self {
  }
}

// 推導結果
impl Foo {
  fn method<'a, 'b>(&'a self, b: &'b i32) -> &'a Self {
  }
}

若不符合上面任一條規則,則必須要標註型態。

如果我們把以上的規則套用在上面的範例 max 上:

fn max(a: &i32, b: &i32) -> &i32 {
  if a > b {
    a
  } else {
    b
  }
}

套用規則 1 :

fn max<'a, 'b>(a: &'a, i32, b: &'b i32) -> &i32 {
  if a > b {
    a
  } else {
    b
  }
}

到這邊結束,編譯器已經沒有可用的規則了,但是回傳值的 lifetime 依然是未知,於是就編譯失敗。

下一篇終於又可以來寫 code 了,同時我們也會介紹 Rust 中的結構,以及上面出現的 impl 是什麼。


上一篇
變數的所有權與借出變數
下一篇
Struct 與 OOP
系列文
30 天深入淺出 Rust33

尚未有邦友留言

立即登入留言