← Back to list

Rust : Lifetime Annotation

看過很多篇文章在說明lifetime Annotation的概念,但感覺蠻多東西都是東拼西湊的,這邊算是紀錄自己學rust的過程,看看能不能幫到一些有跟我相同困惑的人。

Lachie · 2025-10-16 14:59 · 0 claps · 12.0 min read
#rust #lifetime #dangling-pointer
Open on Medium ↗

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還長,那我們就做以下兩件事:

  1. 確認x: & str (這邊是&a)的生命週期沒有比指向的原是數據(a)還要長(我們是全知視角,這點已經確認了)
  2. 回傳&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