iT邦幫忙

2025 iThome 鐵人賽

DAY 8
0

https://ithelp.ithome.com.tw/upload/images/20250917/20124462KA2M7PfuNm.png

Rust 逼我成為更好的工程師:生命週期:借用的時間限制 (2)

在學習 Rust 時,會卡在生命週期的語法上。
但如果我們只專注於要寫 'a 還是 'b,就已經劃錯了重點。

本文不深入探討複雜的語法,規則只有一條——引用(reference)不能比它指向的資料活得更久

過去數十年,無數系統因違反這個規則而崩潰或產生安全漏洞。
C/C++ 把管理的責任交給你,而 Rust 選擇不信任任何人,於是發明了借用檢查器 (Borrow Checker),在編譯階段就找出潛在的懸垂指針 (dangling pointer) 問題。

生命週期標註,本質上就是你與編譯器簽訂的一份「合約」,用來證明你的資料引用關係是安全且清晰的。

生命週期的實際應用總覽

生命週期(Lifetime)不是新發明,它只是把「時間」明確地寫進型別系統,讓你在設計 API 時,對「誰活得更久」做出可檢查的承諾。

本篇三個場景:

  • 結構體包含引用:用 <'a> 把結構體的存活綁到被借用的資料
  • 方法回傳引用:靠「省略規則」(lifetime elision)讓常見樣式零雜訊
  • 靜態生命週期 'static:資料和程式同生命週期,引用永遠有效(在程式期間)

結構體中的引用:簽訂一份「租賃合約」

當你的 struct 欄位包含引用 (&) 時,代表這個結構體並不「擁有」該筆資料,它只是一個「租客」。
身為租客,就必須有一份明確的租約。

// 錯誤的寫法:編譯器無法得知 part 的租期
struct ImportantExcerpt {
     part: &str, // 這個引用從何而來?能存活多久?編譯器無從得知。
}

https://ithelp.ithome.com.tw/upload/images/20250922/20124462kU7mgleaq8.png

// 正確的寫法:明確標示租約 'a
struct ImportantExcerpt<'a> {
    part: &'a str,
}

請不要把 <'a> 視為神秘的符號,它只是一個名字,代表一份租約的期限。

這段宣告的意義是:

「我,ImportantExcerpt,的存活時間不能超過名為 'a 的租約期限。我同時保證,內部的 part 欄位所引用的資料,其生命週期至少與 'a 一樣長。」

讓我們看一個實際例子:

struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    // novel 是資料的「所有者」,如同房產。
    let novel = String::from("Call me Ishmael. Some years ago...");
    
    // first_sentence 是從 novel 借出的一個「引用」,如同房子的鑰匙。
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");

    // i 是一個持有鑰匙的「租客」。
    // 我們必須向編譯器保證,租客的租期 ('a) 不會超過房產 (novel) 的存在時間。
    let i = ImportantExcerpt { part: first_sentence };

    println!("{}", i.part);
} 
// novel 在此處離開作用域 (scope),被銷毀 (drop)。
// 與它關聯的租約 'a 也隨之到期,i 和 first_sentence 自然失效。

https://ithelp.ithome.com.tw/upload/images/20250922/20124462m2i1tKaLGb.png

直觀圖示:

  1. novel 先建立、最後釋放,時間軸最長。
  2. first_sentence 借自 novel,因此其有效期被 novel 上限鎖住。
  3. i.part 借自 first_sentence,所以 i 也必須活在 novel 的有效期內。

如果你試圖讓 i (租客) 活得比 novel (房產) 更久,編譯器會直接拒絕。
它並非在製造麻煩,而是在阻止你拿著一把已經失效的鑰匙,去開一棟已被拆除的房子。這是在從根本上保護你的程式安全。

方法中的生命週期:清晰推斷規則

生命週期省略規則 (lifetime elision) 是一套基於常識的預設行為。

// 1. 必須先定義結構體
struct ImportantExcerpt<'a> {
    part: &'a str,
}

// 2. 然後為它實現方法
impl<'a> ImportantExcerpt<'a> {
    // 純值回傳:與生命週期無關
    fn level(&self) -> i32 {
        3
    }

    // 回傳引用:編譯器根據 &self 自動推斷回傳值的生命週期
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

你寫下的簽名是:

fn announce_and_return_part(&self, announcement: &str) -> &str

編譯器「腦中」會補成:

fn announce_and_return_part<'b>(&'a self, announcement: &'b str) -> &'a str

https://ithelp.ithome.com.tw/upload/images/20250922/20124462mPWza5iwEp.png

也就是說:只要回傳值是引用,且參數中有 &self,就推斷回傳引用跟 self 活一樣久。

圖示解讀:

  1. excerpt 借自 novel,其 'anovel 上限限制。
  2. announce_and_return_part 回傳 self.part,因此回傳值生命週期繼承自 &self(也就是 'a)。
  3. 呼叫端得到的引用,必須活在 novel 的有效期內——不然直接編譯錯誤。

靜態生命週期 'static

'static 是最長的生命週期,代表「與整個程式的執行時間一樣長」。
只有一種類型的資料能天然滿足此條件:硬編碼在執行檔裡的資料,例如字串字面值 (string literal)。

let s: &'static str = "我被編譯進執行檔,程式在,我就在。";
//      ^  ^      ^
//      |  |      |
//      |  |      +---- str: 它指向的資料型別是「字串切片」
//      |  |
//      |  +----------- 'static: 這個承諾,保證引用「永遠有效」
//      |
//      +-------------- &: 這是一個「引用」 (或叫指標、借用)

為什麼它是 'static
因為字串字面值被編譯進可執行檔的只讀區段,程式啟動即存在、結束才釋放。只要你的變數 s 還在作用域中,引用就永遠有效(在程式期間)。

https://ithelp.ithome.com.tw/upload/images/20250922/20124462f9lo5vsmhF.png

如果你試圖用 'static 來安撫編譯器,這通常代表你的設計方向出現了偏差。

這相當於指著一個明天就要過期的麵包,然後對編譯器做出虛假承諾:「相信我,它是永恆的。」

編譯器的錯誤訊息是在提醒你:「你的資料結構有問題!這個引用關聯的資料無法存活那麼久!」。
而使用 'static 只是在為錯誤的結構貼上虛假的標籤。
真正的解決方案是回頭檢視你的資料結構,釐清誰擁有什麼,以及它們各自的生命週期。

總結:別再跟工具打架

  1. 這份資料是誰的? — 搞清楚所有權。

  2. 誰在借用它?能借多久? — 這就是生命週期的本質。

  3. 如果生命週期標註變得越來越複雜,是不是我的設計已經爛掉了? — 答案通常是:是的。

  4. 有沒有更簡單的方法? — 與其搞出一堆複雜的引用關係,不如有時候直接 clone() 一份資料。空間換時間,還能換來更簡單、安全的程式碼。

相關連結與參考資源


上一篇
(Day7) 生命週期:借用的時間限制 (1)
系列文
Rust 逼我成為更好的工程師:從 Borrow Checker 看軟體設計8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言