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

Rust for Python 開發者:Ownership 與 Borrowing 入門

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

一、前言
#

嗨,這裡是拍拍君!🦀

上次我們用 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 是同一個東西!

這很方便,但也帶來問題:

  1. 引用計數有開銷 — 每次賦值都要更新計數器
  2. GC 停頓 — 垃圾回收可能在任何時候暫停你的程式
  3. 共享可變性 — 多個變數指向同一個可變物件,容易出 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:影印一張新借書證給 bab 都能去圖書館借書
  • Rust:把借書證直接交給 ba 手上就沒有了

為什麼要這樣設計?因為 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 還能用!
}

注意幾個重點:

  1. count_words 接收 &str(借用字串),回傳 HashMap(轉移所有權給呼叫者)
  2. top_words 接收 &HashMap(借用),回傳新的 Vec
  3. maincounts 始終是 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

拍拍君的建議:

  1. 先接受 ownership 的概念,不要試圖繞過它
  2. 多用 &,函式參數預設用借用
  3. .clone() 不是壞事,先讓程式跑起來,之後再優化
  4. 聽編譯器的話,它的錯誤訊息真的超好,比大多數語言都友善

記住:跟 Rust 編譯器吵架不是你的問題,是每個 Rust 工程師的日常。但每次吵完,你的程式都會更安全。

下一篇我們來看 Rust 的 structenum,對比 Python 的 dataclassenum——拍拍君保證你會愛上 Rust 的 pattern matching!

Happy coding!🦀✨

延伸閱讀
#

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

相關文章

Rust CLI 實戰:用 clap 打造命令列工具(Python Typer 對照版)
·5 分鐘· loading · loading
Rust Cli Clap Typer Python
Polars:比 Pandas 快 10 倍的 DataFrame 新選擇
·6 分鐘· loading · loading
Python Polars Dataframe 資料分析 Rust
超快速 Python 套件管理:uv 完全教學
·6 分鐘· loading · loading
Python Uv Package Manager Rust
Python 裝飾器:讓你的函式穿上超能力外套
·7 分鐘· loading · loading
Python Decorator 裝飾器 進階語法 設計模式
Python functools 完全攻略:讓函式變得更強大的秘密武器
·7 分鐘· loading · loading
Python Functools Cache Functional-Programming
MLX 入門教學:在 Apple Silicon 上跑機器學習
·4 分鐘· loading · loading
Python Mlx Apple-Silicon Machine-Learning Deep-Learning