出自: 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 協議》,轉載必須註明作者和本文連結