一. 前言 #
寫 Python 的時候,你有想過「這個變數什麼時候被回收」嗎?大概沒有,因為 Python 的垃圾回收器(Garbage Collector, GC)默默幫你處理了一切。你開心地建立物件、丟掉參考,記憶體自然就回來了——多美好。
但如果拍拍君跟你說,有一門語言 完全不需要 GC,卻能保證記憶體安全,你會不會覺得是在騙人?
沒有!這就是 Rust 的 ownership(所有權) 系統。它在編譯階段就把記憶體管理搞定了,不需要 runtime 的 GC、也不會有 C/C++ 那種 dangling pointer 或 double free 的噩夢。
今天就讓拍拍君帶你從 Python 的角度出發,搞懂 Rust 最核心、最重要(也最讓新手崩潰)的概念——ownership 與 borrowing。
二. Python 的記憶體管理:你不知道的幕後英雄 #
在深入 Rust 之前,我們先來看看 Python 是怎麼管記憶體的。
引用計數(Reference Counting) #
Python 的主要記憶體管理策略是 引用計數:
# Python: 引用計數
a = [1, 2, 3] # 建立一個 list,引用計數 = 1
b = a # b 也指向同一個 list,引用計數 = 2
del a # 引用計數 = 1(list 還在!)
del b # 引用計數 = 0 → 立刻回收 ✅
看起來很完美?但引用計數有個致命弱點——循環引用:
# Python: 循環引用的問題
class Node:
def __init__(self):
self.ref = None
a = Node()
b = Node()
a.ref = b # a → b
b.ref = a # b → a(循環了!)
del a
del b
# 引用計數永遠不會歸零 😱
# 需要靠 GC 的「標記清除」來處理
所以 Python 實際上有 兩層 記憶體管理:引用計數 + 循環 GC。這些都是 runtime 的開銷。
GC 的代價 #
GC 很方便,但它有成本:
| 問題 | 說明 |
|---|---|
| 延遲不可預測 | GC 隨時可能暫停你的程式 |
| 記憶體額外開銷 | 每個物件都要存引用計數 |
| 效能損失 | 循環 GC 需要定期掃描 |
| 不適合嵌入式 | 資源有限的環境負擔不起 |
這些在寫 Web app 時可能無所謂,但在系統程式、嵌入式、遊戲引擎等領域就是大問題。
三. Rust 的解法:所有權(Ownership) #
Rust 的做法很激進——在編譯期就決定每塊記憶體什麼時候釋放,完全不需要 runtime GC。
所有權三大規則 #
記住這三條,ownership 就懂了一半:
- 每個值都有一個 owner(擁有者)
- 同一時間只能有一個 owner
- 當 owner 離開 scope,值就被丟掉(drop)
fn main() {
let s1 = String::from("拍拍君"); // s1 是 "拍拍君" 的 owner
println!("{}", s1); // 可以正常使用
} // <- s1 離開 scope,"拍拍君" 被自動釋放(drop)
是不是很像 Python 的引用計數歸零?差別在於:Rust 是在 編譯期 就知道 s1 會在 } 這裡被釋放,不需要任何 runtime 機制。
Move 語意:所有權轉移 #
這是 Python 工程師最容易跌倒的地方:
fn main() {
let s1 = String::from("拍拍君");
let s2 = s1; // 所有權從 s1 轉移到 s2(move)
// println!("{}", s1); // ❌ 編譯錯誤!s1 已經失效了
println!("{}", s2); // ✅ s2 是新的 owner
}
在 Python 裡,b = a 只是讓 b 多一個引用,a 還能繼續用。但在 Rust 裡,let s2 = s1 是 移動——s1 直接失效。
為什麼要這樣設計?因為如果兩個變數同時擁有一塊記憶體,誰來負責釋放?兩個都釋放就 double free,都不釋放就 memory leak。Rust 的做法是:永遠只有一個 owner,問題自然消失。
對比:Python vs Rust #
# Python: 多個引用,共享所有權
a = [1, 2, 3]
b = a # b 和 a 指向同一個 list
a.append(4) # 透過 a 修改
print(b) # [1, 2, 3, 4] — b 也看到了!
fn main() {
let s1 = String::from("hello");
let s2 = s1; // move!s1 失效
// 如果你想要兩份,必須明確 clone
let s3 = s2.clone();
println!("s2 = {}, s3 = {}", s2, s3); // ✅ 各自擁有獨立的副本
}
重點:Rust 不會偷偷幫你複製。要複製就明確呼叫 .clone(),這樣效能開銷一目了然。
四. Copy vs Move:哪些型別例外? #
「等等,那 let x = 5; let y = x; 之後 x 還能用嗎?」
答案是:可以! 因為整數實作了 Copy trait。
fn main() {
let x = 5;
let y = x; // Copy,不是 move
println!("x = {}, y = {}", x, y); // ✅ 兩個都能用
}
哪些型別有 Copy?
| 型別 | Copy? | 說明 |
|---|---|---|
i32, f64, bool, char |
✅ | 基本型別,放在 stack 上 |
(i32, bool) |
✅ | Tuple 裡全是 Copy 型別 |
String |
❌ | 資料在 heap 上,複製成本高 |
Vec<T> |
❌ | 同上 |
簡單規則:放在 stack 上、大小固定、複製便宜的型別才有 Copy。其他都走 move。
用 Python 的話來說,就像 Python 的 int 是 immutable 的(所以 b = a 後修改 b 不影響 a),而 list 是 mutable 的(所以共享引用會互相影響)。
五. Borrowing:不轉移所有權的使用方式 #
每次都 move 或 clone 太不方便了。Rust 提供了 borrowing(借用):我借你用,但所有權還是我的。
不可變引用(&T)
#
fn print_length(s: &String) {
// s 是一個引用(reference),不擁有 String
println!("長度: {}", s.len());
} // s 離開 scope,但因為它不擁有 String,所以什麼都不會被釋放
fn main() {
let s = String::from("拍拍君最棒");
print_length(&s); // 借出 s 的引用
println!("{}", s); // ✅ s 還在,因為只是借出去
}
用 & 建立引用,就是告訴 Rust:「我借你看看,但東西還是我的。」
Python 沒有這個概念——Python 的所有賦值都是引用(某種意義上都是 borrowing),但也因此無法防止「你以為只是看看,結果被改掉了」的問題。
可變引用(&mut T)
#
如果你需要修改借來的東西:
fn add_exclamation(s: &mut String) {
s.push_str("!!!");
}
fn main() {
let mut s = String::from("拍拍君最棒");
add_exclamation(&mut s);
println!("{}", s); // "拍拍君最棒!!!"
}
但 Rust 對可變引用有嚴格限制:
借用規則(超重要!) #
- 同一時間可以有多個不可變引用(
&T) - 同一時間只能有一個可變引用(
&mut T) - 不可變和可變引用不能同時存在
fn main() {
let mut s = String::from("hello");
let r1 = &s; // ✅ 不可變引用 1
let r2 = &s; // ✅ 不可變引用 2
println!("{} {}", r1, r2);
// r1 和 r2 在這之後不再使用(NLL: Non-Lexical Lifetimes)
let r3 = &mut s; // ✅ 現在可以拿可變引用了
r3.push_str(" world");
println!("{}", r3);
}
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &mut s; // ❌ 編譯錯誤!不能同時有不可變和可變引用
println!("{}", r1);
}
這個規則可以用一句話概括:多讀單寫(multiple readers OR single writer)。跟讀寫鎖(RWLock)的概念一模一樣,只是 Rust 在編譯期就幫你做了。
與 Python 的對比 #
# Python: 沒有借用規則,隨時可以改
data = [1, 2, 3]
ref1 = data
ref2 = data
ref2.append(4) # 透過 ref2 修改
print(ref1) # [1, 2, 3, 4] 😱 ref1 也被影響了
Python 允許這樣做,但也因此容易產生 bug。Rust 的借用規則強制你思考:這個資料到底誰在讀、誰在寫?
六. 常見錯誤與解法 #
錯誤 1:Move 之後繼續使用 #
fn take_ownership(s: String) {
println!("Got: {}", s);
}
fn main() {
let s = String::from("拍拍");
take_ownership(s);
// println!("{}", s); // ❌ s 已經被 move 了
}
解法:用引用,或在函式結束後把所有權還回來。
// 方法 A: 借用
fn borrow_it(s: &String) {
println!("Borrowed: {}", s);
}
// 方法 B: 歸還所有權
fn take_and_give_back(s: String) -> String {
println!("Got: {}", s);
s // 回傳,歸還所有權
}
fn main() {
let s = String::from("拍拍");
borrow_it(&s);
println!("{}", s); // ✅ 還在
let s2 = String::from("拍拍醬");
let s2 = take_and_give_back(s2);
println!("{}", s2); // ✅ 拿回來了
}
錯誤 2:Dangling Reference(懸空引用) #
fn create_string() -> &String { // ❌ 編譯錯誤!
let s = String::from("hello");
&s // s 在函式結束時被 drop,引用指向已釋放的記憶體
}
Rust 編譯器直接擋住你,不讓你犯這種在 C/C++ 裡超常見的錯誤。
解法:直接回傳 String(轉移所有權),不要回傳引用。
fn create_string() -> String {
let s = String::from("hello");
s // 所有權轉移給呼叫者
}
錯誤 3:在迴圈中借用 #
fn main() {
let mut v = vec![1, 2, 3];
for item in &v {
// v.push(*item); // ❌ 不能在迭代中修改 v
println!("{}", item);
}
// 正確做法:收集後再修改
let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
v.extend(doubled);
println!("{:?}", v); // [1, 2, 3, 2, 4, 6]
}
Python 的 for item in lst: lst.append(item) 也會出問題(無窮迴圈),但 Python 只會在 runtime 出事,Rust 在編譯期就阻止你了。
七. 實戰:用 Ownership 思維重構 #
來看一個實際的例子——處理使用者資料:
struct User {
name: String,
email: String,
}
// ❌ 不好的寫法:每次都 clone
fn greet_bad(user: User) -> String {
format!("你好,{}!", user.name)
// user 在這裡被 drop 了,呼叫者再也用不到
}
// ✅ 好的寫法:借用就好
fn greet(user: &User) -> String {
format!("你好,{}!", user.name)
}
// ✅ 需要修改時用 &mut
fn update_email(user: &mut User, new_email: String) {
user.email = new_email;
}
fn main() {
let mut user = User {
name: String::from("chatPTT"),
email: String::from("chat@ptt.cc"),
};
println!("{}", greet(&user));
update_email(&mut user, String::from("chat@new.ptt.cc"));
println!("新信箱: {}", user.email);
}
思考順序:
- 只需要讀取?用
&T - 需要修改?用
&mut T - 需要擁有(例如存起來)?用
T(move) - 需要多份?用
.clone()
八. Ownership 速查表 #
| 操作 | Python | Rust |
|---|---|---|
| 賦值 | 共享引用 | Move(或 Copy) |
| 函式傳參 | 共享引用 | Move(或借用 &T) |
| 多個變數存取 | 隨便 | 多讀單寫規則 |
| 記憶體釋放 | GC 自動處理 | owner 離開 scope |
| 深層複製 | copy.deepcopy() |
.clone() |
| 防止意外修改 | 沒有機制 | 不可變引用 &T |
結語 #
Ownership 是 Rust 的靈魂,也是它跟其他語言最大的不同。學會這套系統,你就能寫出 記憶體安全、沒有 GC 開銷、編譯器幫你抓 bug 的程式。
一開始跟編譯器打架是正常的(拍拍君也被 borrow checker 罵過無數次 😤),但慢慢你會發現:每次編譯錯誤其實都是在幫你找出潛在的 bug。
下一篇我們會聊 lifetime(生命週期)——ownership 的延伸,用來處理「引用能活多久」的問題。敬請期待!