快轉到主要內容
  1. 教學文章/

Rust 生命週期(Lifetime)入門:編譯器教你管記憶體

·8 分鐘· loading · loading · ·
Rust Lifetime Borrowing Memory-Safety Python
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
Rust 入門 - 本文屬於一個選集。
§ 7: 本文

一、前言
#

嗨,我是拍拍君 🦀

如果你已經讀過上一篇 Rust ownership & borrowing,應該已經對所有權和借用有了基本概念。但你在寫 Rust 的時候,一定遇過這種令人崩潰的錯誤訊息:

error[E0106]: missing lifetime specifier

或是更可怕的:

error: lifetime may not live long enough

這就是 Rust 的 生命週期(lifetime) 系統在跟你打招呼了!

在 Python 裡,你從來不需要擔心一個變數的「引用」會指到已經被回收的記憶體——因為 GC(垃圾回收器)會幫你處理一切。但 Rust 沒有 GC,所以它需要一套機制來確保:每個引用都是有效的

這套機制就是 lifetime。今天拍拍君要帶你從頭理解它!

二、為什麼需要生命週期?
#

Python:GC 幫你搞定一切
#

在 Python 裡,你可以隨心所欲地傳遞引用:

def get_name():
    name = "拍拍君"
    return name  # 沒問題,Python 會保留這個字串

result = get_name()
print(result)  # "拍拍君" — 永遠安全

Python 用引用計數 + 循環垃圾回收來管理記憶體。只要還有人在用,物件就不會被回收。

Rust:沒有 GC,編譯器必須確認安全
#

在 Rust 裡,如果你嘗試返回一個區域變數的引用:

fn get_name() -> &str {
    let name = String::from("拍拍君");
    &name  // ❌ 編譯錯誤!name 在函式結束時被 drop
}

這段程式碼無法編譯,因為 nameget_name() 結束時就會被銷毀(drop),如果允許返回它的引用,呼叫者就會拿到一個 懸空引用(dangling reference)——指向已釋放記憶體的指標。

這在 C/C++ 裡是經典的 bug 來源,而 Rust 在編譯期就幫你擋下了。

那怎麼辦?
#

要嘛返回擁有所有權的值:

fn get_name() -> String {
    let name = String::from("拍拍君");
    name  // ✅ 所有權轉移給呼叫者
}

要嘛確保引用指向的資料活得夠久——這就是 lifetime 的工作。

三、生命週期的基本概念
#

什麼是 Lifetime?
#

Lifetime 就是一個引用(reference)有效的範圍。每個引用在 Rust 裡都有一個 lifetime,代表「這個引用可以安全使用多久」。

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;       // -+-- 'b  |
        r = &x;          //  |       |
    }                     // -+       |  ← x 在這裡被 drop
                          //          |
    println!("{}", r);    // ---------+  ← ❌ r 指向已釋放的 x
}

在這個例子中:

  • x 的生命週期是 'b(只活在內層的 {} 裡)
  • r 的生命週期是 'a(活在整個 main 裡)
  • 因為 'b'a 短,rx 被 drop 之後還想使用它 → 編譯錯誤!

Python 對照
#

Python 完全沒有這個問題:

def main():
    r = None
    if True:
        x = 5
        r = x  # Python 裡這是複製值(int 是不可變的)
    print(r)    # 5 — 完全沒問題

因為 Python 的變數是物件的「名牌」,不是直接的記憶體指標。即使 x 離開了 scope,物件本身只要還有人引用就不會被回收。

四、生命週期標註語法
#

大部分時候,Rust 編譯器可以自動推斷 lifetime(就像它能推斷型別一樣)。但有些情況下你需要明確標註。

基本語法
#

生命週期用一個單引號 ' 加一個名稱表示,慣例用 'a'b 等:

&i32        // 一個引用
&'a i32     // 帶有生命週期 'a 的引用
&'a mut i32 // 帶有生命週期 'a 的可變引用

在函式中使用
#

最常見需要標註 lifetime 的場景是:函式接收多個引用,並返回一個引用

// ❌ 編譯錯誤:回傳的引用該跟 x 還是 y 的生命週期?
fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() { x } else { y }
}

編譯器不知道回傳值的 lifetime 應該跟 x 還是 y 一樣,所以你需要標註:

// ✅ 明確標註:回傳值的 lifetime 跟 x 和 y 中較短的那個一樣
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

這裡的 'a 代表:「回傳的引用至少會活得跟 xy較短的那個一樣久」。

使用範例
#

fn main() {
    let string1 = String::from("很長的字串");

    {
        let string2 = String::from("短");
        let result = longest(string1.as_str(), string2.as_str());
        println!("比較長的是:{}", result);  // ✅ 在這裡使用沒問題
    }
    // result 在這裡已經不能用了,因為 string2 已經被 drop
}

Python 完全不需要
#

def longest(x: str, y: str) -> str:
    return x if len(x) > len(y) else y

s1 = "很長的字串"
s2 = "短"
result = longest(s1, s2)
print(result)  # 就是這麼簡單 😌

Python 工程師看到 Rust 的 lifetime 標註通常會覺得很煩——但它正是 Rust 能做到零成本記憶體安全的關鍵。

五、生命週期省略規則
#

好消息是,Rust 1.0 之後加入了 生命週期省略規則(Lifetime Elision Rules),讓你在很多情況下不需要手動標註。

編譯器會按照三條規則自動推斷:

規則一:每個引用參數都有自己的 lifetime
#

// 你寫的:
fn first_word(s: &str) -> &str { ... }

// 編譯器理解為:
fn first_word<'a>(s: &'a str) -> &str { ... }

規則二:如果只有一個引用參數,回傳值的 lifetime 就是它
#

// 套用規則二後:
fn first_word<'a>(s: &'a str) -> &'a str { ... }
// 完整推斷完成!不需要手動標註 ✅

規則三:如果有 &self&mut self,回傳值的 lifetime 就是 self
#

impl MyStruct {
    // 你寫的:
    fn name(&self) -> &str {
        &self.name
    }
    // 編譯器自動推斷 lifetime 跟 &self 一樣
}

如果三條規則都套完還是不能確定回傳值的 lifetime,編譯器就會要求你手動標註。

用 Python 的方式理解
#

想像 Python 有一個「嚴格模式」,函式簽名要標註引用的有效範圍:

# 虛擬的 "Rust 風格 Python"
def first_word(s: Ref['a, str]) -> Ref['a, str]:  # lifetime 自動推斷
    ...

def longest(x: Ref['a, str], y: Ref['a, str]) -> Ref['a, str]:  # 必須手動標
    ...

六、結構體中的 Lifetime
#

當結構體持有引用時,你必須標註 lifetime:

// 這個結構體持有一個字串切片的引用
struct Excerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence;

    {
        let i = novel.find('.').unwrap_or(novel.len());
        first_sentence = Excerpt {
            part: &novel[..i],
        };
    }

    // ✅ 可以使用,因為 novel 還活著
    println!("摘錄:{}", first_sentence.part);
}

Excerpt<'a> 的意思是:「這個 Excerpt 實例不能活得比它引用的資料更久」。

幫結構體實作方法
#

impl<'a> Excerpt<'a> {
    // 規則三:回傳值 lifetime 跟 &self 一樣
    fn level(&self) -> i32 {
        3
    }

    // 規則三:回傳的 &str lifetime 跟 &self 一樣
    fn announce_and_return(&self, announcement: &str) -> &str {
        println!("注意:{}", announcement);
        self.part
    }
}

Python 對照
#

from dataclasses import dataclass

@dataclass
class Excerpt:
    part: str  # Python 不需要管 lifetime,GC 搞定

novel = "Call me Ishmael. Some years ago..."
excerpt = Excerpt(part=novel[:novel.find('.')])
print(f"摘錄:{excerpt.part}")

七、靜態生命週期 'static
#

有一個特殊的 lifetime:'static,代表「活到程式結束」。

// 字串字面值都是 'static
let s: &'static str = "我活到程式結束";

// 這也是為什麼字串字面值的型別是 &str 而不是 String
// 它們被嵌入在程式的二進位檔中

常見的 'static 使用場景
#

// 1. 字串字面值
let greeting: &'static str = "你好";

// 2. 全域常數
static LANGUAGE: &str = "Rust";  // 隱含 'static

// 3. 錯誤訊息
fn get_error() -> &'static str {
    "發生了可怕的事情"
}

⚠️ 注意事項
#

當編譯器建議你加 'static 的時候,先停下來想一想——大多數時候你不是真的需要 'static,而是你的 lifetime 標註有其他問題。

// 🔴 不要無腦加 'static
fn longest(x: &'static str, y: &'static str) -> &'static str { ... }
// 這樣限制太嚴格了!只能接受字串字面值

// ✅ 用泛型 lifetime 才對
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { ... }

八、實戰範例:Config Parser
#

讓我們用一個實際的例子來整合所有概念:

/// 一個簡單的設定檔解析器
struct Config<'a> {
    raw: &'a str,
}

impl<'a> Config<'a> {
    fn new(raw: &'a str) -> Config<'a> {
        Config { raw }
    }

    /// 從設定中取得指定 key 的值
    fn get(&self, key: &str) -> Option<&'a str> {
        for line in self.raw.lines() {
            if let Some(rest) = line.strip_prefix(key) {
                if let Some(value) = rest.strip_prefix('=') {
                    return Some(value.trim());
                }
            }
        }
        None
    }

    /// 取得所有 key-value 對
    fn entries(&self) -> Vec<(&'a str, &'a str)> {
        self.raw
            .lines()
            .filter_map(|line| {
                let (key, value) = line.split_once('=')?;
                Some((key.trim(), value.trim()))
            })
            .collect()
    }
}

fn main() {
    let config_text = String::from(
        "name=拍拍君\nversion=1.0\nlanguage=Rust"
    );

    let config = Config::new(&config_text);

    if let Some(name) = config.get("name") {
        println!("名稱:{}", name);  // 名稱:拍拍君
    }

    for (key, value) in config.entries() {
        println!("{}{}", key, value);
    }
}

注意 get 方法的回傳型別是 Option<&'a str> 而不是 Option<&str>——這裡明確標註回傳的引用 lifetime 跟原始設定字串一樣,而不是跟 &self 一樣。這很重要!

Python 版本
#

class Config:
    def __init__(self, raw: str):
        self.raw = raw

    def get(self, key: str) -> str | None:
        for line in self.raw.splitlines():
            if line.startswith(f"{key}="):
                return line.split("=", 1)[1].strip()
        return None

    def entries(self) -> list[tuple[str, str]]:
        result = []
        for line in self.raw.splitlines():
            if "=" in line:
                k, v = line.split("=", 1)
                result.append((k.strip(), v.strip()))
        return result

config = Config("name=拍拍君\nversion=1.0\nlanguage=Rust")
print(config.get("name"))  # 拍拍君

Python 版本簡潔很多,但 Rust 版本有一個巨大的優勢:零記憶體分配。所有的 &str 都是指向原始字串的切片,沒有任何複製或額外的堆積分配。

九、常見錯誤與解法
#

錯誤 1:回傳區域變數的引用
#

// ❌
fn make_greeting(name: &str) -> &str {
    let greeting = format!("你好,{}!", name);
    &greeting  // greeting 在函式結束時被 drop
}

// ✅ 返回擁有的 String
fn make_greeting(name: &str) -> String {
    format!("你好,{}!", name)
}

錯誤 2:生命週期不匹配
#

// ❌
fn first_or_default<'a>(s: &'a str, default: &str) -> &'a str {
    if s.is_empty() {
        default  // 💥 default 的 lifetime 不是 'a
    } else {
        s
    }
}

// ✅ 讓兩個參數的 lifetime 一致
fn first_or_default<'a>(s: &'a str, default: &'a str) -> &'a str {
    if s.is_empty() { default } else { s }
}

錯誤 3:結構體 outlive 引用的資料
#

// ❌
fn create_excerpt() -> Excerpt {
    let text = String::from("Hello world");
    Excerpt { part: &text }  // text 在這裡被 drop!
}

// ✅ 讓資料活得夠久
fn main() {
    let text = String::from("Hello world");
    let excerpt = Excerpt { part: &text };  // text 活得比 excerpt 久 ✅
    println!("{}", excerpt.part);
}

十、Lifetime 速查表
#

場景 需要標註? 範例
函式只有一個引用參數 ❌ 自動推斷 fn len(s: &str) -> usize
方法有 &self ❌ 自動推斷 fn name(&self) -> &str
多個引用參數,返回引用 ✅ 需要標註 fn longest<'a>(x: &'a str, y: &'a str) -> &'a str
結構體持有引用 ✅ 需要標註 struct Foo<'a> { s: &'a str }
不返回引用 ❌ 不需要 fn print(s: &str)

結語
#

Rust 的 lifetime 系統一開始確實讓人頭痛,尤其是從 Python 轉過來的開發者。但它帶來的好處是巨大的:

  • 零成本安全:不需要 GC,不需要引用計數,記憶體安全在編譯期保證
  • 零 dangling pointer:永遠不會存取已釋放的記憶體
  • 明確的資料流:lifetime 標註讓你的程式碼更加文件化

拍拍君的建議是:先習慣讓編譯器幫你推斷,遇到錯誤時再學著標註。大多數時候,省略規則已經夠用了!

下一篇我們會介紹 Rust collections(Vec, HashMap),看看 Rust 的集合型別跟 Python 的 list/dict 有什麼不同。到時候見!🦀

延伸閱讀
#

Rust 入門 - 本文屬於一個選集。
§ 7: 本文

相關文章

Rust for Python 開發者:Ownership 與 Borrowing 入門
·7 分鐘· loading · loading
Rust Python Ownership Borrowing Memory
Rust Ownership & Borrowing:跟 Python GC 說再見
·8 分鐘· loading · loading
Rust Ownership Borrowing Python 記憶體管理
Rust 錯誤處理:Result/Option vs Python try/except
·7 分鐘· loading · loading
Rust Python Error-Handling Result Option
Rust struct 與 enum:對比 Python dataclass 與 Enum
·9 分鐘· loading · loading
Rust Python Struct Enum Pattern-Matching Dataclass
Rust CLI 實戰:用 clap 打造命令列工具(Python Typer 對照版)
·5 分鐘· loading · loading
Rust Cli Clap Typer Python
Rust trait vs Python ABC/Protocol:抽象介面大比拼
·8 分鐘· loading · loading
Rust Python Trait Abc Protocol 泛型 Interface