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

Rust Ownership & Borrowing:跟 Python GC 說再見

·8 分鐘· loading · loading · ·
Rust Ownership Borrowing Python 記憶體管理
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
Rust 入門 - 本文屬於一個選集。
§ 6: 本文

一. 前言
#

寫 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 就懂了一半:

  1. 每個值都有一個 owner(擁有者)
  2. 同一時間只能有一個 owner
  3. 當 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 對可變引用有嚴格限制:

借用規則(超重要!)
#

  1. 同一時間可以有多個不可變引用(&T
  2. 同一時間只能有一個可變引用(&mut T
  3. 不可變和可變引用不能同時存在
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);
}

思考順序:

  1. 只需要讀取?用 &T
  2. 需要修改?用 &mut T
  3. 需要擁有(例如存起來)?用 T(move)
  4. 需要多份?用 .clone()

八. Ownership 速查表
#

操作 Python Rust
賦值 共享引用 Move(或 Copy)
函式傳參 共享引用 Move(或借用 &T
多個變數存取 隨便 多讀單寫規則
記憶體釋放 GC 自動處理 owner 離開 scope
深層複製 copy.deepcopy() .clone()
防止意外修改 沒有機制 不可變引用 &T

結語
#

Ownership 是 Rust 的靈魂,也是它跟其他語言最大的不同。學會這套系統,你就能寫出 記憶體安全、沒有 GC 開銷、編譯器幫你抓 bug 的程式。

一開始跟編譯器打架是正常的(拍拍君也被 borrow checker 罵過無數次 😤),但慢慢你會發現:每次編譯錯誤其實都是在幫你找出潛在的 bug。

下一篇我們會聊 lifetime(生命週期)——ownership 的延伸,用來處理「引用能活多久」的問題。敬請期待!

延伸閱讀
#

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

相關文章

Rust for Python 開發者:Ownership 與 Borrowing 入門
·7 分鐘· loading · loading
Rust Python Ownership Borrowing Memory
Rust trait vs Python ABC/Protocol:抽象介面大比拼
·8 分鐘· loading · loading
Rust Python Trait Abc Protocol 泛型 Interface
Rust 錯誤處理:Result/Option vs Python try/except
·7 分鐘· loading · loading
Rust Python Error-Handling Result Option
Rust CLI 實戰:用 clap 打造命令列工具(Python Typer 對照版)
·5 分鐘· loading · loading
Rust Cli Clap Typer Python
Rust struct 與 enum:對比 Python dataclass 與 Enum
·9 分鐘· loading · loading
Rust Python Struct Enum Pattern-Matching Dataclass
Polars:比 Pandas 快 10 倍的 DataFrame 新選擇
·6 分鐘· loading · loading
Python Polars Dataframe 資料分析 Rust