Rust 生命週期 - lifetime in fn

dreamfine發表於2020-05-29

出自: www.zhihu.com/column/rust-shen

生命週期 ? 什麼玩意兒?我搞這麼多年程式設計,怎麼沒聽說過?Rust真是事媽,我恨它!

稍安勿躁。來,綁起來,打一針,且聽我《維為道來》

其實,這要上升到哲學高度。少裝B,簡單點。好,你想過死嗎?你是不是經常,以一萬年為單位,來規劃自己的生活?任何事情都有生生滅滅的過程,只是你沒在意。

象golang, java,python,這樣的語言中,GC替你做了一切。它管理變數的生命週期。到了某個節點,就象大清算來了一樣,GC收回了必要的記憶體。在它忙活這件事兒的時侯,整個”世界”似乎都暫停了。所以,這正是GC程式讓人討厭的地方,會卡住一下下。

象C就牛了,它才不管你這事,你自己管理記憶體的使用和銷燬。錯了它也不知道,你也蒙圈。

Rust使用生命週期的概念,以原語的方式,讓我們明確定義和使用lifetime。

輕鬆夠了吧,來個第一印象:

lifetime 是放在尖括號裡的。Rust 的 lifetime 可以當做泛型系統的一部分, 或叫它泛型生命週期引數:

foo<'a, 'b>     // 含義:foo的生命週期不能超出 'a 和 'b 中任一個的週期。

fn f<'a, 'b>(v1: &'a T1, v2: &'b T2) -> &'a T3 {...}

這裡面的 ‘a ‘b 是泛型引數,在這裡的意思就是一種約束,傳入兩個指標 v1 v2,返回一個指標。v1 的 lifetime 和 v2 不同,但是和最終返回的指標相同。

使用生命週期,很大的原因,是我們關注這樣的問題:

實體A持有一個指向實體B的引用,則A能夠訪問期間B必須存活;

為什麼你的C語言經常莫名其妙的掛掉,很可能就是懸空指標:人活著,錢沒了。

還是以例子形式展開:

//正確: 一個引數,編譯器能推斷生命週期,不用註明 
fn longest1(x: &str) -> &str {
    x
}

//錯誤: 二個引數,這讓編譯器犯迷糊。即使沒使用,也不行,編譯不過去
fn longest2(x: &str,y:&str) -> &str {
    print!("{}",y);
    x
}

longest2錯誤提示:expected named lifetime parameter(渴望 命名 生命週期 引數)。

它需要你 顯式的註明 生命週期,正確的方式是這樣的:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

來個測試:

#[test]
fn test_longest(){
    let out =longest("abc","qwert");
    println!("---> {} <---",out);
}

沒問題。這樣做標記有什麼用呢? 標記後,生命週期會更長一些嗎?
不會。只是編譯器要求,生命週期必須有明確的定義,它不清楚時,你得告訴它。
如果有多個引數,你就得告訴編譯器返回的引用是跟著哪個引數的生命週期。

為了引入下一個知識點,我設計一種情景:

fn foo() {
    let x = 520; //本來我希望caller呼叫這個520
    {
        let x = 1314; //不小心,我在一個子域中 多寫了這麼一句
        let out = caller(x);
        println!("---> {} <---", out); //此時我們引入了一個錯誤,但編譯器並沒有發現
    }   
}

如何避免上例中的錯誤出現?那就是現在介紹的,這個東西:

fn foo<'a: 'b, 'b>     //  <-   'a: 'b 讀作 'a 生命週期至少和 'b 一樣長。

我不知道怎麼稱呼它,有的資料上說,這是強制轉換。我感覺有點bullshit。變數的生命週期是由它所在的位置決定的,設計的能隨意改動,不合情理。這個標記,應該是一種對外部引數的一種要求和限制。就象說,我要求穿泳裝才能進這個門,而不是你如果穿長裙,我會給你脫掉,換成泳裝。
上個例子吧:

fn choose_first<'a, 'b:'a>(first: &'a i32, second: &'b i32) -> &'a i32 {
    println!("---> second={}",second);
    first 
}
#[test]
fn test_force() {
    let var_a = 23; // 較長的生命週期
    let var_b = 520; // 注意這個變數,它並不是choose_first的引數
    let out:& i32 ;
    {
        let var_b = 1314;  //var_b定義在這裡會出錯。
        //因為choose_first 的定義中 有 'b:'a, 意思就是要求b和a的生命一樣長。
        out =  choose_first(&var_a, &var_b); 
        println!("1---> var_b = {}", var_b);
    };
    println!("---> {} is the first", out);
    println!("2---> var_b = {}", var_b);
}

因為我們在定義choose_first中作了限制,choose_first<’a,’b:’a>要求’b變數至少要和’a變數活的一樣久。所以,假如寫了let var_b =1314; 編程式碼的眼瞎,編譯器可不會放過。

我們來設計一個反證例子:

fn choose_first<'a:'b, 'b>(first: &'a i32, second: &'b i32) -> &'a i32 {
    println!("---> second={}",second);
    first 
}

#[test]
fn test_force_bad(){
    let var_b=12;
    let out;  
    {
        let var_a=34;
        out =choose_first(&var_a,&var_b); //編譯不通過,提示 &var_a 有問題
    }
    println!("---> {} <---",out);
}

在這個例子中, var_a的壽命較短,出界後,out就死掉了。即便我們加了 ‘a: ‘b 也是一點毛用沒有。 這個例子充分證明了我上面的說法是正確的。 線上把玩

進一步,想多一點:

看上去,這種手工標記生命週期的方式,就象手動檔汽車 一樣。
難道不能智慧推斷嗎?選擇最短的生命週期嘛,編譯器應該能算出來。
可能是考慮到編譯耗時,以及檢查不一定有那麼智慧。
而且在函式相互遞迴的情況下,編譯並不能推斷真正的返回結果,仍然需要annotation。
所以不如手工標記。而且我感覺這個負擔也不大。
反而有個好處,讓寫程式碼的,更加清醒的看清楚自己的程式碼邏輯,以及危險的邊界。
還有,象我們剛才提到的 ‘a: ‘b 這種良好的要求和限制,你不手工指定,編譯器怎麼會知道。
所謂當局者迷,我看旁觀者也未必清,很可能是瞎比哄哄。

看看別人怎麼說:
所以如果函式有遞迴呼叫可能就沒法推導了。
雖然不能全部推導,但是部分推導還是能實現的。
rust沒有這麼做可能是擔心效率問題吧,
很多C++程式碼靜態分析工具做了類似的事情,那用起來真的很慢。

不只函式,生命週期標註,也會出現在struct 、trait、impl裡面。不說了頭大。

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

相關文章