107.06.02 rust 之路 10 生命週期

哇,直接富堅超過半個月 QuQ

好了這應該是最後一個比較偏向觀念的地方

有一天,書與報紙只會剩下一個,我會希望的是我與報紙
因為我無法想像她一個人如何對抗孤獨
--- 呱吉                
Photo by Fabrizio Verrecchia on Unsplash

最基本的生命週期(Lifetime)概念

為什麼要有 lifetime,主要是因為有 borrow (reference),但又不希望 dangling

我們先來回想一下 borrow
let y;
{
    let x = "OuO";
    y = &x;
}
println!("{}", y);

在上面程式中,y 在 {} 中跟 x 借用了 "OuO"
也就是 y 是指向 x 的 reference
不過會出現以下編譯錯誤
error[E0597]: `x` does not live long enough
 --> life.rs:5:14
  |
5 |         y = &x;
  |              ^ borrowed value does not live long enough
6 |     }
  |     - `x` dropped here while still borrowed
7 |     println!("{}", y);
8 | }
  | - borrowed value needs to live until here

應該有很明顯地指出問題,x 在 } 就離開有效範圍
此時 x 結束生命週期,擁有的數值隨之消失 (drop)
但是 y 還在借用,不過借用的東西已經消失,因此產生 dangling reference
而這個不安全的操作被編譯器擋下來了

此例子可看出必須要保證被借用者 (x) 的生命週期必須大於借用者 (y) 才行
此例子沒有特別的解決方式
因為這是寫程式的人的錯,本來就不應該產生任何 dangling reference 或 pointer

Borrow Checker

向剛剛的例子中,Rust 編譯器中會有 Borrow Checker 來確認 lifetime 的操作
lifetime 為 'a 的 y 借到生命週期為 'b 的 x,明顯看出 'b 活不過 'a 因此產生錯誤訊息
{
    let y;                // ---------+-- 'a
    {                     //          |
        let x = "OuO";    // -+-- 'b  |
        y = &x;           //  |       |
    }                     // -+       |
    println!("y: {}", y); //          |
}                         // ---------+

那這樣總可以了吧:
{
    let y;             // ---------+-- 'a
    let x = "OuO";     // -+-- 'b  |
    y = &x;            //  |       |
    println!("{}", y); //  |       |
}                      // -+-------+ 'b 比 'a 早結束

答案還是不行,因為 drop 的順序與產生順序相反 ('b 比 'a 早一點點點結束)
所以 x 的值會先 drop,接著才是 y,然後就發現 y 借用的 x 已經不在了
正確要如下
{
    let x = "OuO";     // ---------+-- 'b
    let y = &x;        // -+-- 'a  |
    println!("{}", y); //  |       |
}                      // -+-------+ 'b 比 'a 晚結束

標示 Lifetime

要標示 lifetime 時使用單引號再加小寫的單字 e.g. 'a, 'this_a_lifetime
不過大部分都用一個小寫字母描述
通常需要標示的會有 function, struct, trait

函式中的 Lifetime

函式有 borrow 當然也會有 lifetime 的問題
對於較簡單的 funcrion 可以省略 lifetime,編譯器對於 lifetime 會自行推論
// implicit
fn foo(x: &i32) {}

// explicit
fn bar<'a>(x: &'a i32) {}

那可以被推論的 "簡單 funcrion" 是怎麼簡單?
Lifetime elision 有以下三個規則:
(以下參數講的是 reference 參數)
1. 所有輸入的參數若省略 lifetime,都會被配上不同的 lifetime
2. 恰一個輸入的參數(有無省略),輸出參數若省略 lifetime 都會與輸入的相同
3. 方法有 &Self 或 &mut Self,輸出參數若省略 lifetime 都與 &Self 的 lifetime 相同
(其他就會出現錯誤訊息)

'static

這是一個特殊的生命週期,代表具有整個程式的生命週期
通常會使用在全域或是字串上 (&str)
let x: &'static str = "OuO";

static FIVE: i32 = 5;
let x: &'static i32 = &FIVE;

例子

可省略

expanded 為 compiler 接收到 elided 形式會自動擴展
fn foo(s: &str);        // elided
fn foo<'a>(s: &'a str); // expanded

非 reference 就不需要標示 lifetime,因為它們通常會移動所有權或複製
若 struct 內部有 reference 也須標示 lifetime
fn foo(i: u32, s: &str);        // elided
fn foo<'a>(i: u32, s: &'a str); // expanded

fn get_mut(&mut self) -> &mut T;           // elided
fn get_mut<'a>(&'a mut self) -> &'a mut T; // expanded

fn args<T: ToCStr>(&mut self, args: &[T]) -> &mut Command; // elided
fn args<'a, 'b, T: ToCStr>(&'a mut self, args: &'b [T]) -> &'a mut Command; // expanded

fn new(buf: &mut [u8]) -> BufWriter;            // elided
fn new<'a>(buf: &'a mut [u8]) -> BufWriter<'a>; // expanded

不可省略

fn get_str() -> &str; // ILLEGAL, no inputs

fn frob(s: &str, t: &str) -> &str;               // ILLEGAL, two inputs
fn frob<'a, 'b>(s: &'a str, t: &'b str) -> &str; // Expanded: Output lifetime is ambiguous

看看以下函式,功能就是比長度,然後回傳長的字串
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}

會出錯:原因就是因為 Lifetime elision 的規則
此時 x, y 會附上不同的 lifetime 'a, 'b 回傳時就不知道要附上哪一個,如下面錯誤訊息
error[E0106]: missing lifetime specifier
 --> src/main.rs:1:33
  |
1 | fn longest(x: &str, y: &str) -> &str {
  |                                 ^ expected lifetime parameter
  |

解決方式就是要改成這樣,即不能省略 lifetime 標示
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

<'a> 代表此函式有使用一個 lifetime 'a
後面有參考的參數或回傳值都加上 'a
運作方式就如下圖,使用時必須保證 s、t 的 lifetime 長於 u

沒有留言:

張貼留言

^ Top