一、前言 #
嗨,這裡是拍拍君!🦀
上次我們用 clap 打造了 CLI 工具,體驗了 Rust 的威力。但如果你寫 Rust 超過五分鐘,一定會遇到這位老朋友:
error[E0382]: borrow of moved value: `s`
恭喜你,你正式踏入了 Rust 最核心、最獨特、也最讓人抓狂的領域——Ownership(所有權) 和 Borrowing(借用)。
別怕!拍拍君今天要用 Python 工程師聽得懂的方式,把這個概念講清楚。一旦搞懂它,你就掌握了 Rust 80% 的精髓。
二、Python 怎麼管記憶體? #
在 Python 裡,你幾乎不用想記憶體的事。Python 用引用計數 + 垃圾回收來管理:
# Python:自動管理記憶體
a = [1, 2, 3]
b = a # b 和 a 指向同一個 list
b.append(4)
print(a) # [1, 2, 3, 4] — 因為 a 和 b 是同一個東西!
這很方便,但也帶來問題:
- 引用計數有開銷 — 每次賦值都要更新計數器
- GC 停頓 — 垃圾回收可能在任何時候暫停你的程式
- 共享可變性 — 多個變數指向同一個可變物件,容易出 bug
# 共享可變性的陷阱
def process_data(data):
data.sort() # 直接修改了原始資料!
return data[:5]
original = [3, 1, 4, 1, 5, 9, 2, 6]
top5 = process_data(original)
print(original) # [1, 1, 2, 3, 4, 5, 6, 9] — 原始資料被改了 😱
Rust 的態度是:這些問題不該存在。它用一套編譯期規則來消除這些問題,不需要 GC,也不會有共享可變性的坑。
三、Ownership:每個值只有一個主人 #
Rust 的第一條規則很簡單:
每個值只有一個 owner(擁有者),owner 離開作用域時值被釋放。
用 Python 的思維來對照:
# Python:多個變數可以指向同一個物件
a = "hello"
b = a # a 和 b 都「擁有」這個字串(引用計數 +1)
print(a) # 完全沒問題
print(b) # 也沒問題
// Rust:賦值 = 轉移所有權(move)
fn main() {
let a = String::from("hello");
let b = a; // 所有權從 a 轉移到 b
// println!("{a}"); // ❌ 編譯錯誤!a 已經不擁有這個值了
println!("{b}"); // ✅ 只有 b 能用
}
等等,a = "hello" 在 Python 裡超正常,為什麼 Rust 把 a 搞不見了?
Move 的直覺理解 #
把 ownership 想像成借書證:
- Python:影印一張新借書證給
b,a和b都能去圖書館借書 - Rust:把借書證直接交給
b,a手上就沒有了
為什麼要這樣設計?因為 Rust 保證任何時刻只有一個 owner,就不需要引用計數,也不可能出現「兩個人同時修改同一本書」的問題。
Copy 型別:例外中的例外 #
有些簡單的型別(整數、浮點數、布林、字元)不會 move,而是自動複製:
fn main() {
let x = 42;
let y = x; // 複製,不是 move
println!("{x}"); // ✅ 完全沒問題
println!("{y}"); // ✅
}
為什麼?因為整數就是個數字,複製它跟搬家一樣便宜(就像抄一個號碼)。但 String 是存在 heap 上的資料,複製它很貴,所以 Rust 預設用 move。
對照表:
| 型別 | Rust 行為 | Python 類比 |
|---|---|---|
i32, f64, bool |
Copy(自動複製) | 不可變型別(int, float) |
String, Vec<T> |
Move(轉移所有權) | 可變型別(list, dict) |
四、函式與 Ownership #
在 Python 裡,把東西丟進函式很隨意:
def print_list(data):
print(data)
my_list = [1, 2, 3]
print_list(my_list)
print(my_list) # 完全沒問題,my_list 還在
在 Rust 裡,傳進函式 = 轉移所有權:
fn print_vec(data: Vec<i32>) {
println!("{:?}", data);
} // data 在這裡被釋放!
fn main() {
let my_vec = vec![1, 2, 3];
print_vec(my_vec);
// println!("{:?}", my_vec); // ❌ my_vec 已經被 move 進函式了!
}
這⋯⋯也太不方便了吧?難道每次呼叫函式都要放棄我的資料?
當然不是。這就是 Borrowing 登場的時候了。
五、Borrowing:借我用一下,不要拿走 #
Borrowing 就是「借用」——你可以讓別人看你的書,但書還是你的。
不可變借用(&T)
#
fn print_vec(data: &Vec<i32>) { // 注意 & 符號:借用,不是擁有
println!("{:?}", data);
} // data 的借用結束,但原始資料不會被釋放
fn main() {
let my_vec = vec![1, 2, 3];
print_vec(&my_vec); // 傳入引用
println!("{:?}", my_vec); // ✅ my_vec 還在!
}
Python 對照:
# Python 本來就是傳引用,所以沒有這個問題
# 但 Rust 的好處是:借用期間保證不會被修改!
你可以同時有多個不可變借用:
fn main() {
let s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &s;
println!("{r1}, {r2}, {r3}"); // ✅ 多個人同時看書,沒問題
}
可變借用(&mut T)
#
如果你要修改資料,需要可變借用:
fn add_item(data: &mut Vec<i32>, item: i32) {
data.push(item);
}
fn main() {
let mut my_vec = vec![1, 2, 3];
add_item(&mut my_vec, 4);
println!("{:?}", my_vec); // [1, 2, 3, 4] ✅
}
但 Rust 有一條嚴格的規則:
同一時間,要嘛有多個不可變借用,要嘛只有一個可變借用,不能兩者並存。
fn main() {
let mut s = String::from("hello");
let r1 = &s; // ✅ 不可變借用
let r2 = &s; // ✅ 多個不可變借用 OK
// let r3 = &mut s; // ❌ 不能在有不可變借用時做可變借用!
println!("{r1}, {r2}");
// r1 和 r2 到這裡就不再使用了(NLL: Non-Lexical Lifetimes)
let r3 = &mut s; // ✅ 現在可以了!
r3.push_str(" world");
println!("{r3}");
}
為什麼要這麼嚴格? #
用 Python 的例子來解釋。回想前面的 process_data 陷阱:
# Python 允許這種事發生
data = [3, 1, 4, 1, 5]
ref1 = data # 「不可變借用」— 我只是要讀取
data.sort() # 「可變借用」— 同時修改!
print(ref1) # 結果被改了,ref1 看到的不是原來的值
Rust 的規則在編譯期就阻止了這種 bug。不是靠測試、不是靠 code review,是根本不讓你寫出來。
六、實戰:用 Ownership 重構一個 Python 程式 #
讓我們把一個常見的 Python 程式改寫成 Rust,感受 ownership 的實際運作。
Python 版:統計單字頻率 #
from collections import Counter
def count_words(text: str) -> dict[str, int]:
words = text.lower().split()
return dict(Counter(words))
def top_words(counts: dict[str, int], n: int = 5) -> list[tuple[str, int]]:
return sorted(counts.items(), key=lambda x: -x[1])[:n]
text = "the rust book says the rust compiler is the best compiler"
counts = count_words(text)
top = top_words(counts, 3)
print(top) # [('the', 3), ('rust', 2), ('compiler', 2)]
Rust 版 #
use std::collections::HashMap;
fn count_words(text: &str) -> HashMap<String, usize> {
let mut counts = HashMap::new();
for word in text.to_lowercase().split_whitespace() {
*counts.entry(word.to_string()).or_insert(0) += 1;
}
counts
}
fn top_words(counts: &HashMap<String, usize>, n: usize) -> Vec<(String, usize)> {
let mut sorted: Vec<_> = counts.iter()
.map(|(k, v)| (k.clone(), *v))
.collect();
sorted.sort_by(|a, b| b.1.cmp(&a.1));
sorted.truncate(n);
sorted
}
fn main() {
let text = "the rust book says the rust compiler is the best compiler";
let counts = count_words(text); // counts 擁有 HashMap
let top = top_words(&counts, 3); // 借用 counts,不拿走
println!("{:?}", top);
println!("總共 {} 個不同的字", counts.len()); // ✅ counts 還能用!
}
注意幾個重點:
count_words接收&str(借用字串),回傳HashMap(轉移所有權給呼叫者)top_words接收&HashMap(借用),回傳新的Vecmain裡counts始終是 owner,想用幾次就用幾次
如果把 top_words 改成接收 HashMap(不加 &):
fn top_words(counts: HashMap<String, usize>, n: usize) -> Vec<(String, usize)> {
// counts 被 move 進來了
// ...
}
fn main() {
let counts = count_words(text);
let top = top_words(counts, 3); // counts 被 move 走了
// println!("{}", counts.len()); // ❌ 編譯錯誤!
}
經驗法則:函式只需要讀取資料時,用 &(借用);需要擁有資料時,才用 move。
七、Lifetime:借用的有效期限 #
當你開始寫返回借用的函式,就會遇到 lifetime(生命週期):
// 這個函式回傳一個借用,但 Rust 需要知道它的有效期限
fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() >= s2.len() { s1 } else { s2 }
}
fn main() {
let s1 = String::from("hello");
let result;
{
let s2 = String::from("hi");
result = longer(&s1, &s2);
println!("{result}"); // ✅ s1 和 s2 都還活著
}
// println!("{result}"); // ❌ s2 已經離開作用域了!
// // result 可能指向 s2,所以不安全
}
'a 是什麼?它告訴編譯器:回傳的借用至少要跟輸入活得一樣久。
用 Python 的角度想:
# Python 不會有這問題,因為有 GC
# 但如果你用過 C/C++,你一定踩過回傳 dangling pointer 的坑
# Rust 的 lifetime 就是在編譯期防止這種事
什麼時候需要寫 lifetime? #
大多數時候不用!Rust 有 lifetime elision(生命週期省略) 規則,會自動推導:
// 不用寫 lifetime — 編譯器自動推導
fn first_word(s: &str) -> &str {
s.split_whitespace().next().unwrap_or("")
}
// 只有多個輸入借用且回傳也是借用時,才需要手動標註
fn longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() >= s2.len() { s1 } else { s2 }
}
新手建議:先不要糾結 lifetime,等編譯器要求你寫的時候再加就好。
八、常見錯誤與解法 #
錯誤 1:Move after use #
let s = String::from("hello");
let s2 = s;
println!("{s}"); // ❌ value borrowed here after move
解法: 用 .clone() 明確複製
let s = String::from("hello");
let s2 = s.clone(); // 明確複製
println!("{s}"); // ✅
錯誤 2:同時可變和不可變借用 #
let mut v = vec![1, 2, 3];
let first = &v[0]; // 不可變借用
v.push(4); // ❌ 可變借用(push 需要 &mut self)
println!("{first}");
解法: 調整使用順序
let mut v = vec![1, 2, 3];
let first = v[0]; // 複製值(i32 是 Copy 型別)
v.push(4); // ✅
println!("{first}"); // ✅
錯誤 3:回傳區域變數的引用 #
fn make_greeting() -> &str {
let s = String::from("hello");
&s // ❌ s 在函式結束時被釋放,引用會指向空氣
}
解法: 回傳擁有的值
fn make_greeting() -> String {
String::from("hello") // ✅ 轉移所有權給呼叫者
}
九、Ownership 設計模式速查表 #
| 情境 | 建議 | 範例 |
|---|---|---|
| 函式只需讀取 | 用 &T 借用 |
fn len(s: &str) -> usize |
| 函式需要修改 | 用 &mut T |
fn push(v: &mut Vec<i32>) |
| 函式需要擁有 | 直接傳 T |
fn consume(s: String) |
| 需要多個 owner | 用 Rc<T> 或 Arc<T> |
進階主題 |
| 要複製一份 | 用 .clone() |
let s2 = s.clone(); |
| 簡單型別 | 自動 Copy | let y = x; |
十、Python vs Rust 記憶體管理總比較 #
| 面向 | Python | Rust |
|---|---|---|
| 記憶體管理 | GC(引用計數 + 循環檢測) | Ownership(編譯期) |
| 共享資料 | 多個引用,隨便改 | 要嘛多讀,要嘛一寫 |
| 安全性 | 執行期才發現問題 | 編譯期就抓到 |
| 效能 | GC 停頓 | 零成本抽象,無 GC |
| 學習曲線 | 🟢 低(不用想記憶體) | 🔴 高(要跟編譯器溝通) |
| 執行速度 | 慢(10-100x) | 快(接近 C) |
結語 #
Ownership 和 borrowing 是 Rust 最讓人又愛又恨的特色:
- 又恨:編譯器一直跟你吵架,寫個簡單程式都要想半天
- 又愛:一旦編譯過了,你的程式幾乎不會有記憶體相關的 bug
拍拍君的建議:
- 先接受 ownership 的概念,不要試圖繞過它
- 多用
&,函式參數預設用借用 .clone()不是壞事,先讓程式跑起來,之後再優化- 聽編譯器的話,它的錯誤訊息真的超好,比大多數語言都友善
記住:跟 Rust 編譯器吵架不是你的問題,是每個 Rust 工程師的日常。但每次吵完,你的程式都會更安全。
下一篇我們來看 Rust 的 struct 和 enum,對比 Python 的 dataclass 和 enum——拍拍君保證你會愛上 Rust 的 pattern matching!
Happy coding!🦀✨