本文 AI 產出,尚未審核

Rust 課程 – 生命週期

單元:函數中的生命週期


簡介

在 Rust 中,**生命週期(Lifetime)**是編譯器保證記憶體安全的核心機制之一。即使在沒有垃圾回收器的情況下,Rust 仍能在編譯期檢查出可能的懸掛指標(dangling pointer)或資料競爭(data race),這全賴於對生命週期的精確描述。

當我們把 參考(reference) 作為函式參數或返回值時,必須讓編譯器知道這些參考的有效範圍。若未正確標註,程式將無法通過編譯,或在執行時產生未定義行為。掌握函式中的生命週期標註,不僅能讓程式碼順利編譯,更能提升可讀性與維護性,對初學者與中級開發者都相當重要。


核心概念

1. 為什麼需要生命週期標註?

Rust 的借用檢查器(borrow checker)在編譯期會追蹤每一個參考的「活躍」時間。對於 單純的局部變數,編譯器可以自動推斷生命週期;但當參考跨越函式邊界(例如作為參數或回傳值)時,編譯器就需要 顯式的生命週期參數 來說明它們之間的相對長短。

簡單來說:如果兩個參考的生命週期無法由編譯器自行判斷,就必須手動加上 'a'b… 之類的標記。

2. 基本語法

fn foo<'a>(x: &'a i32) -> &'a i32 {
    x
}
  • 'a生命週期參數,放在函式名稱後的尖括號中。
  • &'a i32 表示「這個參考在 'a 生命週期內有效」。
  • 函式的返回值也使用同樣的 'a,代表 返回的參考與輸入參考同活同止

3. 多個參考的生命週期關係

當函式同時接受多個參考時,必須明確說明它們之間的關係:

fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1.len() > s2.len() { s1 } else { s2 }
}
  • 兩個參數都使用同一個 'a,代表 兩者的生命週期至少要相同長,否則編譯器會報錯。
  • 若其中一個參數的生命週期較短,則必須使用不同的生命週期參數,或使用 生命週期省略規則(稍後說明)。

4. 生命週期省略規則(Lifetime Elision)

對於最常見的情況,Rust 提供了三條省略規則,使得開發者不必每次都寫 'a

  1. 每個輸入參考皆獲得自己的獨立生命週期。
  2. 若只有一個輸入參考,返回值會自動取得該參考的生命週期。
  3. 若有多個輸入參考,返回值的生命週期會與 第一個 輸入參考相同。
// 省略生命週期的寫法,編譯器會自動套用規則
fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
}

5. 生命週期與結構體(Struct)

函式中常會傳入或回傳包含參考的結構體,此時生命週期必須在結構體定義上標註:

#[derive(Debug)]
struct Slice<'a> {
    part: &'a [i32],
}

fn make_slice<'a>(data: &'a [i32]) -> Slice<'a> {
    Slice { part: data }
}

程式碼範例

範例 1:最簡單的生命週期標註

fn echo<'a>(msg: &'a str) -> &'a str {
    // 直接回傳參考,編譯器知道返回值與參數同活
    msg
}

fn main() {
    let text = String::from("Hello, Rust!");
    let r = echo(&text);
    println!("{}", r); // Hello, Rust!
}

註解'a 讓編譯器知道 r 的生命週期不會超過 text,因此安全。

範例 2:兩個參考的比較(Longest)

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

fn main() {
    let a = "short";
    let b = "much longer string";
    let result = longest(a, b);
    println!("Longest: {}", result); // "much longer string"
}

重點:若 ab 的生命週期較短,編譯器會直接拒絕,避免回傳已失效的參考。

範例 3:使用不同生命週期的參數

fn combine<'a, 'b>(first: &'a str, second: &'b str) -> String {
    // 兩個參考的生命週期互不相關,返回值是擁有所有權的 String
    format!("{} {}", first, second)
}

fn main() {
    let s1 = String::from("Rust");
    let s2 = "Lang";
    let merged = combine(&s1, s2);
    println!("{}", merged); // "Rust Lang"
}

說明:返回值不再是參考,而是 擁有所有權String,因此不需要把 'a'b 合併。

範例 4:結構體與生命週期

#[derive(Debug)]
struct Borrowed<'a> {
    name: &'a str,
    data: &'a [u8],
}

fn make_borrowed<'a>(name: &'a str, data: &'a [u8]) -> Borrowed<'a> {
    Borrowed { name, data }
}

fn main() {
    let name = "payload";
    let bytes = &[1, 2, 3, 4];
    let obj = make_borrowed(name, bytes);
    println!("{:?}", obj);
}

要點Borrowed 的每個欄位都必須在同一個 'a 生命週期內有效,否則會產生編譯錯誤。

範例 5:生命週期省略規則的實際應用

fn first_char(s: &str) -> &str {
    // 省略 `'a`,編譯器自動把返回值的生命週期與參數相同
    &s[0..1]
}

fn main() {
    let txt = "Rust";
    let c = first_char(txt);
    println!("First char: {}", c); // "R"
}

提醒:省略規則只能在符合三條規則的情況下使用,若不符合必須手動標註。


常見陷阱與最佳實踐

陷阱 說明 解決方式
生命週期衝突 多個參考的生命週期互相重疊,導致編譯器無法推斷最長者。 明確使用不同的生命週期參數(如 'a'b),或改寫為擁有所有權的型別。
返回局部參考 函式返回指向局部變數的參考。 永遠不要返回局部變數的參考;改為返回 StringVec<T> 等擁有所有權的資料。
過度標註 為每個參考都寫 'a,即使編譯器已能自動推斷。 利用 生命週期省略規則,保持程式碼簡潔。
結構體內部生命週期不一致 結構體欄位使用不同的生命週期卻只標註一個 'a 為每個欄位使用獨立的生命週期參數,或將欄位改為擁有所有權的型別。
迭代器與生命週期 在迭代器中返回參考時,常會碰到 “cannot return reference to temporary value”。 使用 Iterator::map 搭配 into_iter,或將結果收集到 Vec<T> 後再返回。

最佳實踐

  1. 先嘗試省略:只有在編譯器報錯時才加入顯式生命週期。
  2. 盡量返回擁有所有權的資料:減少生命週期標註的複雜度。
  3. 使用 #[derive(Debug)] 觀察結構體的生命週期行為,方便除錯。
  4. 在 API 設計上保持一致:若函式接受多個相同生命週期的參考,統一使用同一個 'a,讓呼叫端更易理解。

實際應用場景

  1. 文字處理庫:如 fn split<'a>(s: &'a str) -> impl Iterator<Item=&'a str>,需要保證切割後的子字串與原字串同活。
  2. 圖形渲染引擎:在渲染管線中,VertexBuffer<'a> 會持有對記憶體區塊的參考,必須與渲染指令的生命週期同步。
  3. 資料庫查詢fn query<'a>(conn: &'a Connection, sql: &'a str) -> Result<Row<'a>, Error>,返回的 Row 必須在 Connection 存活期間有效。
  4. Web 框架fn handle<'a>(req: &'a Request<'a>) -> Response<'a>,請求與回應的生命週期通常是相同的 HTTP 交易週期。

在上述情況下,正確的生命週期標註不僅防止程式崩潰,也讓 API 使用者能清楚了解資料的有效範圍,提升整體安全性與可維護性。


總結

  • 生命週期是 Rust 記憶體安全的基石,尤其在函式參數與返回值之間的關係上扮演關鍵角色。
  • 透過 顯式的 'a'b 標註,我們可以告訴編譯器「這些參考在同一段時間內有效」。
  • 省略規則 讓日常開發更簡潔,但在複雜情況下仍須手動標註。
  • 常見陷阱包括返回局部參考、生命週期衝突與結構體內部不一致,解決方法是使用擁有所有權的型別或正確分配生命週期參數。
  • 在文字處理、渲染、資料庫與 Web 框架等實務領域,正確的生命週期設計能提升 API 的安全性與可讀性。

掌握 函式中的生命週期,不僅能讓你的 Rust 程式順利編譯,更能寫出更安全、更具表現力的程式碼。祝你在 Rust 的旅程中,玩得開心、寫得安心!