一、前言 #
嗨,我是拍拍君 🦀
如果你已經讀過上一篇 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
}
這段程式碼無法編譯,因為 name 在 get_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短,r在x被 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 代表:「回傳的引用至少會活得跟 x 和 y 中較短的那個一樣久」。
使用範例 #
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 有什麼不同。到時候見!🦀