本文 AI 產出,尚未審核

Rust – 生命週期

單元:結構體中的生命週期


簡介

在 Rust 中,所有權借用生命週期 共同構成了記憶體安全的核心機制。當我們把資料封裝進結構體時,結構體本身的生命週期會受到其成員的生命週期影響。若結構體裡保存的是引用&T),編譯器必須知道這些引用在什麼時候有效,才能防止 懸垂指標(dangling reference)產生。

本篇文章將以 結構體中的生命週期 為主題,從基本語法到實務應用,逐步說明如何正確地在結構體上使用生命週期標註,並提供常見陷阱與最佳實踐,幫助讀者在日常開發中寫出安全且易於維護的程式碼。


核心概念

1. 為什麼結構體需要生命週期

當結構體的欄位是借用&T&mut T)時,編譯器無法自行推斷這些引用的存活時間。若沒有明確的生命週期標註,編譯器會產生 “cannot infer an appropriate lifetime” 的錯誤。

重點生命週期標註不是額外的負擔,而是 Rust 用來保證程式在編譯期即已排除記憶體錯誤的工具。

2. 基本語法

// 定義一個持有字串切片的結構體,必須標註生命週期 'a
struct Message<'a> {
    content: &'a str,
}
  • 'a 表示「在此結構體實例被建立之前,content 所指向的字串必須已經存在」。
  • 結構體本身的生命週期會自動被推斷為 'a,因此 Message<'a> 只能在 'a 仍然有效時使用。

3. 結構體方法與生命週期

impl<'a> Message<'a> {
    // 方法本身可以不需要額外的生命週期標註,因為它已經繼承了結構體的 'a
    fn len(&self) -> usize {
        self.content.len()
    }

    // 若回傳一個引用,需要明確說明返回值的生命週期
    fn first_word(&self) -> &'a str {
        self.content.split_whitespace().next().unwrap_or("")
    }
}
  • &self 隱含了 'a,所以 self 的生命週期與結構體相同。
  • first_word 回傳的 &'a str 必須與結構體的生命週期相同,否則編譯器無法保證安全。

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

對於函式的參數與返回值,Rust 允許省略生命週期標註,只要遵守三條省略規則。結構體本身則不支援省略,必須明確寫出。

// 雖然這是函式,但展示省略規則的寫法
fn get_len(s: &str) -> usize {
    s.len()
}
  • 省略規則不適用於結構體的欄位,因為結構體可能被存放在不同的作用域中,編譯器無法推斷其生命週期。

5. 多個生命週期參數

有時候結構體需要同時保存多個不同來源的引用:

struct Pair<'a, 'b> {
    first: &'a i32,
    second: &'b i32,
}

impl<'a, 'b> Pair<'a, 'b> {
    fn sum(&self) -> i32 {
        *self.first + *self.second
    }
}
  • 'a'b 可以是不同的生命週期,只要在使用 Pair 時保證兩者同時有效即可。

6. 使用 Box<T>Rc<T> 取代引用

如果結構體的生命週期標註過於繁瑣,考慮改用擁有所有權的類型:

use std::rc::Rc;

struct OwnedMessage {
    content: Rc<String>, // 共享所有權,不需要生命週期標註
}
  • Rc<T>(或 Arc<T>)讓多個結構體共享同一筆資料,而不必擔心借用的生命週期。

程式碼範例

範例 1:最簡單的引用結構體

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

fn demo() {
    let text = String::from("Hello, Rust!");
    let s = Slice { part: &text[..5] }; // part = "Hello"
    println!("slice: {}", s.part);
} // `s` 在此離開作用域,`text` 仍然有效

說明Slice 必須標註 'a,否則編譯會錯誤。

範例 2:帶有多個生命週期的結構體

struct TwoRefs<'a, 'b> {
    left: &'a str,
    right: &'b str,
}

fn demo_two() {
    let a = "first";
    let b = String::from("second");
    let pair = TwoRefs { left: a, right: &b };
    println!("{} {}", pair.left, pair.right);
}

說明'a'b 可以分別對應不同的來源,只要兩者在 pair 使用期間都活著即可。

範例 3:在 impl 中返回引用

struct Text<'a> {
    data: &'a str,
}

impl<'a> Text<'a> {
    fn first_word(&self) -> &'a str {
        self.data.split_whitespace().next().unwrap_or("")
    }
}

fn demo_first() {
    let sentence = "Rust is awesome";
    let t = Text { data: sentence };
    println!("first word: {}", t.first_word());
}

說明:返回值的生命週期必須與 self 相同,否則會產生 “cannot return reference to temporary value” 的錯誤。

範例 4:使用 Box<T> 取代生命週期

struct Owned<'a> {
    // 需要生命週期標註的寫法
    // part: &'a str,
    // 改用 Box,編譯器不再要求標註
    part: Box<str>,
}

fn demo_owned() {
    let txt = String::from("Owned data");
    let o = Owned { part: txt.into_boxed_str() };
    println!("owned: {}", o.part);
}

說明Box<str> 取得了資料的所有權,結構體不再依賴外部的生命週期。

範例 5:結構體與 Rc<T> 的共享所有權

use std::rc::Rc;

#[derive(Debug)]
struct SharedMsg {
    content: Rc<String>,
}

fn demo_rc() {
    let msg = Rc::new(String::from("Shared message"));
    let a = SharedMsg { content: Rc::clone(&msg) };
    let b = SharedMsg { content: Rc::clone(&msg) };
    println!("{:?} / {:?}", a, b);
    // 此時 msg 的引用計數為 3,離開作用域後才會釋放
}

說明Rc<T> 讓多個結構體同時持有同一筆資料,省去繁瑣的生命週期標註。


常見陷阱與最佳實踐

陷阱 可能的錯誤訊息 解決方式
忘記在結構體上加生命週期 missing lifetime specifier 為每個引用欄位加上 'a(或多個)
返回的引用生命週期過短 cannot return reference to temporary value 確保返回值的生命週期與 self 或參數相同
多個生命週期衝突 lifetime mismatch 使用 where 子句或重新設計資料結構,使生命週期相容
過度使用引用 編譯時間變長、代碼難以閱讀 若資料所有權不會頻繁搬移,改用 Box<T>Rc<T>String 等擁有型別
使用 &'static str 卻期待可變 cannot assign mutable reference to immutable data &'static 只能指向程式生命週期內不變的資料,若需變動請改用 StringArc<Mutex<T>>

最佳實踐

  1. 盡量使用擁有型別StringVec<T>Box<T>)來降低生命週期標註的複雜度。
  2. 只在必要時才標註生命週期,避免在所有結構體上都加上 'static,這會削弱安全性。
  3. 將生命週期限制放在 impl 區塊,而不是在每個方法上重複寫。
  4. 利用 where 子句 讓生命週期與型別界限更清晰:
impl<'a> Message<'a>
where
    'a: 'static, // 例:要求 'a 至少與 'static 同長
{
    // 方法實作
}
  1. 測試與文件化:在公開 API 前,寫下說明哪個參數的生命週期必須長於哪個,並在單元測試中驗證。

實際應用場景

  1. 文字解析器(Parser)

    • 解析大型檔案時,常會返回字串切片 (&'a str) 以避免拷貝。此時 Token<'a> 結構體保存切片,必須與原始檔案的生命週期同步。
  2. GUI 元件

    • UI 元件可能只需要顯示資料的引用,而不擁有資料本身。Label<'a> 結構體保存 &'a str,只要 UI 的存活時間不超過資料來源即可。
  3. 快取系統

    • 快取結構體 Cache<'a, K, V> 可能保存對外部資料結構的引用,以減少記憶體使用。此時多個生命週期(鍵、值)需要同時管理。
  4. 多執行緒共享

    • 使用 Arc<T> 搭配 RwLock<T>,在結構體中保存 Arc<RwLock<Data>>,可以在多執行緒間安全共享,而不必手動追蹤生命週期。

總結

  • 結構體中的生命週期 是 Rust 保證記憶體安全的關鍵概念,尤其在欄位使用引用時必須明確標註。
  • 透過 'a'b 等標註,我們告訴編譯器「這些引用在何時有效」,從而避免懸垂指標與資料競爭。
  • 實務上,若資料的所有權可以被取得(StringBox<T>Rc<T>),優先使用擁有型別,減少生命週期標註的負擔。
  • 常見的陷阱包括忘記標註、返回過短的引用以及多生命週期衝突;遵循 最佳實踐(最小化引用、適時使用擁有型別、清晰的文件說明)即可有效避免。

掌握了結構體的生命週期後,你將能在 解析器、GUI、快取等 需要高效記憶體管理的領域,寫出既安全又高效的 Rust 程式碼。祝你在 Rust 的旅程中玩得開心、寫得安心!