Rust : Lifetime Annotation
看過很多篇文章在說明lifetime Annotation的概念,但感覺蠻多東西都是東拼西湊的,這邊算是紀錄自己學rust的過程,看看能不能幫到一些有跟我相同困惑的人。
Rust : Lifetime Annotation
看過很多篇文章在說明lifetime Annotation的概念,但感覺蠻多東西都是東拼西湊的,這邊算是紀錄自己學rust的過程,看看能不能幫到一些有跟我相同困惑的人。
先備知識
這篇文章適合誰閱讀? 知道rust中的所有權概念、了解引用用法(&)、但被lifetime annotation 用法搞得暈頭轉向的初學者(ex: me)。
生命週期 lifetime
先從基本的生命週期開始說明。所謂生命週期就是一個變數「活多久」。這個概念本身很簡單,看看以下範例
fn main() {
let a = String::from("hello"); //---------------------------------------|
{ // ---| |
let temp = String::from("world"); // | |
// | | I am a's lifetime !!
} //----| I am temp's lifetime !! |
} //------------------------------------------------------------------------|
非常簡單,就是你所在的那個 brack (括號)、或是函數內部,就是你存活的時間,也就是叫做「作用域」。
在rust每一個變數(不管是一般或是reference)都需要有對應的生命週期資訊。是一定要有,當你放眼望去看到一堆變數,第一件事就是盤問他: 你的生命週期護照呢? (一旦發現過期或是不合法,理應立刻清除它!!)
除它!!)
一旦某個變數脫離作用域,他就是死去了。如果你還繼續想要呼叫他,喔喔不好意思,編譯器會直接抱錯。試試看以下code,看看他說了甚麼?
fn main() {
let a = String::from("hello");
{
let temp = String::from("world");
}
println!("{}", temp) ; // error!
}
OK,會報錯。但是我們要稍微延伸一下這個概念。
我們知道,在rust的世界裡面有一般型別的數據、和所謂reference參考型別的數據,靠他我們可以在不轉移所有權的狀態下,查看或是修改指向的數據。
一般型別數據的lifetime : 就和前面說的一樣,存在於其宣告的括號範圍內裡面。
參考(reference)型別數據lifetime : 嘿嘿,沒想過這件事吧? 參考型別數據lifetime稍微複雜,因為它和指向的原始數據有綁定關係,所以限制較多。
在以下三種情況下,參考型別數據的lifetime會被消滅:
1. 作用域結束
和一般型別一樣,誰作用域結束都會死去
fn main() {
let s = String::from("Hello");
{
let r = &s ;
println!("{}", r) ;
}
// r is dead
}
2. 原始數據改動
不論你是可變參考、不可變參考,當你指向的原始數據改變了,參考都會自動失效
fn main() {
let s = String::from("Hello") ;
let r = & mut s ;
s.push_str(" world") ; // r die, program error(借用出去之後,s本身不能改值)
}
3. 原始數據生命週期結束
這個情境比較難想像一點,當你的參考還存在,但是指向的原始數據已經死了。可以看看以下例子,這比較不常見,但是確實會發生。在C++中這沒什麼大不了的,頂多就是給你一個null,但是在注重記憶體管理rust中,這是嚴重的,有個詞專門形容這種情況: dangling pointer(懸停參考)。
記住這句關鍵話: 參考活的比指向的原始數據久。bad,rust 不喜歡這樣…
fn main() {
let r;
{
let s = String::from("Hello");
r = &s;
} // s die, but r alive, program error
}
實際上針對前兩個情境,rust 都有100%的機制可以處理收回記憶體空間,或是偵錯,第三種情境雖然也有可以對應的工具,但因為這種情況比較複雜,rust的機制會不小心把一些看起來「可能造成dangling pointer」的情況也擋下來(當然實際上是合法的)。所以life annotation就是一種輔助,來讓rust可以更加精準的辨識當前的情況到底是不是dangling pointer,或是只是長的很像但是不是的情況。
所以這個小節我們知道lifetime annotation 的作用了,但是實際上到底是怎麼做呢?在下一個小節說明。
生命週期標籤lifetime annotation
lifetime annotation 這個東西的出現完全是因為function 回傳的東西是reference。如果今天回傳的東西是不是reference,或是根本沒有回傳,實際上那麼就不需要notation。但是一旦是以reference 的方式回傳,那麼就和前面提到的dangling pointer有關,就一定要用life notation來標記。
接下來,我們體驗一下用人類(全知視角)、以及function(局部視角),的觀點來看看以下這個程式。
注意: 以不同視角觀察是這篇文章最核心的地方
fn main() {
let a = String::from("hello world");
let temp = String::from("world");
let a_p = &a ;
let temp_p = &temp ;
let result = longest(a_p, temp_p);
println!("{}", result);
// complier error !
}
fn longest(x: & str, y: & str) -> &str {
if x.len() > y.len() { x } else { y }
}
這段code在做事就是,傳入兩個字串的參考(reference)然後想要回傳比較長的字串的參考。
以全知觀點來看 : 觀察a_p和temp_p,這兩個東西的在main裡面創立,他們的生命週期屬於main內部。在main 裡面,我們確實掌握了這兩個參考的生命週期。當我們把這兩個參考送進longest內部,我們還是清楚的知道這兩個東西生命週期的資訊。So? 我們可以確定a_p和temp_p不會超過他們指向地原始數據地生命週期,當longest比對出來之後把比較長的那個參考(這邊是a_p)的生命週期賦予變數result。
用例子來說明:說如果你最後發現a_p指向的東西hello world 比 temp_p 指向的world還長,那我們就做以下兩件事:
- 確認
x: & str(這邊是&a)的生命週期沒有比指向的原是數據(a)還要長(我們是全知視角,這點已經確認了) - 回傳&a給result
一切看似合理又簡單。 錯!!!!!!!!!!
以function的觀點來看: 當longest 接收到兩個莫名其妙的參考時,他並沒有清楚接收到生命週期資訊!!!!!!!!!!!!!!(實際上其實有,但是rust 要求顯式標記,但後面會在解釋,先當作沒有)。他並不知道在外部的main到底發生甚麼事。他只是徒然的、意外的,接收到兩個看起來沒有生命週期護照的參考。
還記得我們前面說的嗎? 當他開始進行邏輯運算、結束準備要回傳資訊時,同樣要執行剛剛提到的兩件事,但他發現: 第一步是要檢查我拿到的這個x: & str 是否有超過指向的原始數據的生命週期(這個例子是a),但是我們發現壓根兒沒拿到啊x的生命週期資訊啊???
這時候編譯器就發現出大事了… 沒辦法比較x到底有沒有可能超過其指向地原始數據的生命週期,所以上面的code就拋出error了。 這就是rust的設計哲學: 我不管你main 後續會做任何處理,但是我堅持在在我longest內部就先確保這個返還地的參考數據不會有dangling pointer 的風險。
OK,發現問題,一開始longest function 就沒拿到生命週期資訊(x、y 都沒拿到)。那麼補上去是不是就解決了?沒錯! 這時候你就可以發現
‘a指的是 「x 和 y 所指向的原始資料的生命週期」
fn main() {
let a = String::from("hello world");
let temp = String::from("world");
let a_p = &a ;
let temp_p = &temp ;
let result = longest(a_p, temp_p) ;
println!("{}", result);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn longest<'a>: 我這裡有一個’a標籤
x: &'a str: 我指向的東西(這邊指a)的生命週期(這邊只a對應的生命週期範圍是main)
y: &'a str: 我指向的東西(這邊指temp)的生命週期(這邊只temp對應的生命週期範圍是main)
&'a str: 我這個function 回傳的東西的生命週期,必須等於‘a的範圍(這邊是main這個範圍)。注意!這邊是等於喔,不能是大於或是小於。
直面的解讀是這樣。回想一下我們剛剛提到的dangling pointer。rust 極度討厭dangling pointer,所以她正在做的事就是: 防止參考的生命週期大過於其所指向的原始數據的生命週期,也就是'a所標記的main 範圍。
看到了嗎?'a標籤提供了一個參考點,longest可以依據這個參考點去檢查你回傳的這個東西的lifetime範圍必須要和'a 一樣,如果不一樣就直接GG。
多標籤與實戰分析
主要謎團已經解開了,但是還有一些延伸和實際操演沒說。有些直覺敏銳的小鬼說話了,假設我今天傳入的兩個參考生命週期不一樣呢?
fn main() {
let a = String::from("hello world"); //a 的生命週期在main
let a_p = &a ;
{
let temp = String::from("world"); //temp 生命週期只在內括弧
let temp_p = &temp ;
let result = longest(a_p, temp_p); // a_p 所指向的a、temp_p所指向的temp 生命週期不一樣
}
println!("{}", result);
}
最討厭你這種直覺敏銳的小鬼… 回歸正題。a_p 所指向的a、temp_p所指向的temp 生命週期顯然不一樣。而規則是這樣的,同一個標籤只能代表一組生命週期,所以既然你的生命週期不一樣,那你就要用不同標籤 fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'b str
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &'b str {
if x.len() > y.len() { x } else { y }
}
OK,這樣可以編譯過了對嗎? 但是仔細看這一段code,實際上他還是編譯不過的,but why?
這邊longest回傳的東西的生命週期可能是’a or ‘b ,蘊含著這樣的不確定性,但是我們已經規定輸出的資料必定是’b 的生命週期型態。所以嚴格的rust編譯一定不會過。這時小鬼又問: 蛤!那到底甚麼時候是有多個生命週期標籤(lifetime annotations),但是可以正確編譯成功的呢? 答案是,當你確定你的輸出100%是’a 或是100% ‘b 就可以正確編譯。所以rust嚴謹的一面下,其實對於這種條件式造成的不確定性其實蠻嚴苛的。所以當出現不確定回傳格式生命週期的方式就是,盡量讓兩個或多個傳入的參考有相同的生命週期,這樣不管選到哪一個都有一樣的生命週期可以通過編譯。
釋疑
還記得我們剛剛有說道,實際上function longest有接收到兩個參考的生命週期嗎? 但是為甚麼我必須要特別標記才能編譯過呢?
實際上rust 是聰明的,當你今天只有一個參考輸入的時候
fn main() {
let a = String::from("hello world");
let a_p = &a ;
let result = longest(a_p);
println!("{}", result);
}
fn longest(x: &str) -> & str { //只有一個參考輸入
println!("{}", x) ;
x
}
rust 可以自動幫你標記(隱式標記),你不用手動標記,但是如果你有多個參考傳入,他會要求你要顯式標記,以講求整體的安全性,所以實際上這是雙層保護,除了他其實有接收到生命週期資訊之外,你也要手動標記才可以喔!
Struct 的lifetime notation
這是一篇補充,因為後來發現lifetime notation 在struct 和 function 的功能上不太一樣,所以特別回來這篇補充一下。我們可以看一下在function 中,lifetime annotation 關注的是:
function 中 reference 所指向的物件的lifetime, 以及輸出的reference的lifetime
fn main() {
let a = String::from("hello world");
let b = String::from("hello test") ;
let a_p = &a ;
let b_p = &b ;
let result = test(a_p);
println!("{}", result);
}
fn test <'a>(x: &'a str, y: &'a str) -> &'a str {
println!("{}", x) ;
x
}
以這個例子來說我確保的是「x、y這兩個references 指向的物件(a、b)的lifetime」和我回傳的這個reference(a_p)一樣長。這和test這個function 本身沒什麼關係。
換句話說,如果函式回傳的不是 reference(&),那就不需要寫 lifetime notation。
但是struct 的lifetime notation 就和 function 不一樣,struct 中的notation 關注
Struct 中 reference 所指向的物件的lifetime,和這個struct中本身的lifetime
fn main() {
let n = "kim" ;
let t = Test {n, 38} ;
}
struct Test<'into> {
name: &'info str ,
age: i32 ,
}
以這個例子來說我確保的是「Test 裡的 name這個reference 指向的物件(n)的lifetime」和我這個struct t 一樣長(避免t還活著、但n死了)。 這著實的和 t 這個struct有關係對吧?
所以凡struct有用到&當作型別,一定要加入notation。 沒有任何時候是可以不寫的,因為不寫實直接影響到整個struct 的存在性。
메타데이터
- post_id
- bd76414abe6c
- slug
- rust-lifetime-annotation-bd76414abe6c
- url
- https://medium.com/@king0209king0209/rust-lifetime-annotation-bd76414abe6c
- canonical_url
- https://medium.com/@king0209king0209/rust-lifetime-annotation-bd76414abe6c
- author_url
- https://medium.com/@king0209king0209
- status
- ok
- fetched_at
- 2026-07-07 00:45:30