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

Rust 智慧指標:Box、Rc、Arc 完全攻略

·8 分鐘· loading · loading · ·
Rust Smart Pointers Box Rc Arc Memory Management
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
Rust 入門 - 本文屬於一個選集。
§ 10: 本文

一、前言
#

在 Python 的世界裡,我們幾乎不需要考慮「這個東西放在 stack 還是 heap」——因為 Python 的所有東西都在 heap 上,垃圾回收器會幫我們搞定一切。

但 Rust 不一樣。Rust 預設把值放在 stack(堆疊)上,只有明確要求時才會放到 heap(堆積)。而當我們需要 heap 配置、共享所有權、或多執行緒共享資料時,就需要用到 智慧指標(smart pointers)

今天拍拍君要帶大家認識 Rust 三大智慧指標:

智慧指標 功能 Python 類比
Box<T> 將值放到 heap 上 所有 Python 物件(預設就在 heap)
Rc<T> 單執行緒共享所有權(引用計數) Python 的引用計數 sys.getrefcount()
Arc<T> 多執行緒共享所有權(原子引用計數) 多執行緒環境的引用計數

如果你正在跟著 Rust 入門系列 一路學過來,搞懂了所有權生命週期,那智慧指標就是你的下一個里程碑 🎯

二、Stack vs Heap:快速複習
#

在開始之前,讓我們先釐清 stack 和 heap 的差異:

Stack(堆疊)                    Heap(堆積)
┌─────────────┐                ┌──────────────────┐
│ 大小固定     │                │ 大小可變          │
│ 配置超快     │                │ 配置較慢          │
│ 自動清除     │                │ 需要手動或智慧管理 │
│ 函式返回即釋放│                │ 可跨作用域存活     │
└─────────────┘                └──────────────────┘

Python 中你不會感受到這個差異,因為 一切都在 heap 上

# Python:這些全部都在 heap 上
x = 42          # heap 上的 int 物件
name = "拍拍君"  # heap 上的 str 物件
data = [1, 2, 3] # heap 上的 list 物件

Rust 則預設放 stack:

fn main() {
    let x = 42;           // stack 上
    let name = "拍拍君";    // 字串字面值在靜態記憶體,引用在 stack
    let data = [1, 2, 3]; // stack 上
}

那什麼時候需要 heap?幾個常見情境:

  1. 資料太大,不適合放 stack
  2. 大小在編譯期未知(遞迴型別、動態大小)
  3. 需要共享所有權(多個地方同時擁有同一份資料)
  4. 需要讓資料活得比當前作用域更久

三、Box<T>:最簡單的智慧指標
#

Box<T> 是最基本的智慧指標——它把值從 stack 移到 heap,並在離開作用域時自動釋放。

基本用法
#

fn main() {
    // 值在 stack 上
    let x = 5;

    // 值在 heap 上,b 是指向它的智慧指標(在 stack 上)
    let b = Box::new(5);

    // 用法跟一般值一樣(自動解引用)
    println!("b = {}", b);       // 印出:b = 5
    println!("b + 1 = {}", *b + 1); // 明確解引用也行
}
// b 離開作用域 → heap 上的記憶體自動釋放

Python 對比:

# Python 中,所有東西都已經在 heap 上
# 所以你根本不需要 Box 這種東西
x = 5  # 已經在 heap 了!

實際用途一:遞迴型別
#

Box 最經典的用途是定義 遞迴型別。沒有 Box,Rust 編譯器無法計算型別的大小:

// ❌ 編譯失敗!Rust 不知道 List 多大
// enum List {
//     Cons(i32, List),
//     Nil,
// }

// ✅ 用 Box 包起來,大小就是固定的指標大小
enum List {
    Cons(i32, Box<List>),
    Nil,
}

use List::{Cons, Nil};

fn main() {
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
    print_list(&list);
}

fn print_list(list: &List) {
    match list {
        Cons(val, next) => {
            print!("{} -> ", val);
            print_list(next);
        }
        Nil => println!("Nil"),
    }
}
// 輸出:1 -> 2 -> 3 -> Nil

在 Python 中,遞迴結構很自然:

# Python 的 linked list,不需要特別處理
class Node:
    def __init__(self, val, next=None):
        self.val = val
        self.next = next

head = Node(1, Node(2, Node(3)))

實際用途二:Trait Object
#

當你需要在一個集合中存放不同型別(但都實作了某個 trait),Box<dyn Trait> 就派上用場:

trait Animal {
    fn speak(&self) -> &str;
}

struct Dog;
struct Cat;

impl Animal for Dog {
    fn speak(&self) -> &str { "汪!" }
}

impl Animal for Cat {
    fn speak(&self) -> &str { "喵~" }
}

fn main() {
    // Vec 裡面放不同型別,用 Box<dyn Trait>
    let animals: Vec<Box<dyn Animal>> = vec![
        Box::new(Dog),
        Box::new(Cat),
        Box::new(Dog),
    ];

    for animal in &animals {
        println!("{}", animal.speak());
    }
}
// 輸出:
// 汪!
// 喵~
// 汪!

Python 版本(天生多型,不需要 Box):

class Dog:
    def speak(self): return "汪!"

class Cat:
    def speak(self): return "喵~"

animals = [Dog(), Cat(), Dog()]
for a in animals:
    print(a.speak())

Box 小結
#

特性 Box<T>
所有權 獨佔(一個 Box 擁有一份資料)
執行緒安全 ✅(如果 T 是 Send)
開銷 極小(就是一個指標)
用途 遞迴型別、trait object、大型資料搬到 heap

四、Rc<T>:共享所有權(單執行緒)
#

有時候你需要 多個變數同時擁有同一份資料。在 Python 中這是預設行為:

import sys

data = [1, 2, 3]
a = data
b = data

# a, b, data 都指向同一份 list
print(sys.getrefcount(data))  # 4(包含 getrefcount 自身的引用)

在 Rust 中,所有權系統規定「每個值只有一個所有者」。但 Rc<T>(Reference Counting)可以打破這個規則——它用 引用計數 來追蹤有多少個 Rc 指向同一份資料:

use std::rc::Rc;

fn main() {
    let data = Rc::new(vec![1, 2, 3]);

    let a = Rc::clone(&data);  // 引用計數 +1
    let b = Rc::clone(&data);  // 引用計數 +1

    println!("引用計數:{}", Rc::strong_count(&data)); // 3

    println!("a = {:?}", a);
    println!("b = {:?}", b);
}
// 所有 Rc 離開作用域 → 引用計數歸零 → 資料釋放

💡 Rc::clone() 不會深拷貝資料,它只是增加引用計數(非常便宜的操作)。

實際用途:共享節點的圖結構
#

use std::rc::Rc;

#[derive(Debug)]
struct Node {
    value: i32,
    children: Vec<Rc<Node>>,
}

fn main() {
    // 共享的子節點
    let shared_child = Rc::new(Node {
        value: 42,
        children: vec![],
    });

    let parent_a = Node {
        value: 1,
        children: vec![Rc::clone(&shared_child)],
    };

    let parent_b = Node {
        value: 2,
        children: vec![Rc::clone(&shared_child)],
    };

    println!("shared_child 被引用 {} 次", Rc::strong_count(&shared_child));
    // 輸出:shared_child 被引用 3 次

    println!("Parent A 的子節點:{:?}", parent_a.children[0].value);
    println!("Parent B 的子節點:{:?}", parent_b.children[0].value);
    // 兩個 parent 共享同一個 child!
}

Rc 的限制
#

⚠️ Rc<T> 有兩個重要限制:

  1. 只限單執行緒——不能跨執行緒傳遞
  2. 預設不可變——如果需要修改共享資料,要搭配 RefCell<T>
use std::rc::Rc;
// use std::thread;

fn main() {
    let data = Rc::new(42);

    // ❌ 編譯失敗!Rc 不能跨執行緒
    // thread::spawn(move || {
    //     println!("{}", data);
    // });

    // ✅ 在同一個執行緒內使用沒問題
    let clone = Rc::clone(&data);
    println!("{}", clone);
}

Rc + RefCell:內部可變性
#

如果你需要共享 修改資料,可以組合 Rc<RefCell<T>>

use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let shared_data = Rc::new(RefCell::new(vec![1, 2, 3]));

    let a = Rc::clone(&shared_data);
    let b = Rc::clone(&shared_data);

    // 透過 a 修改資料
    a.borrow_mut().push(4);

    // 透過 b 也能看到修改
    println!("b 看到的資料:{:?}", b.borrow());
    // 輸出:b 看到的資料:[1, 2, 3, 4]
}

Python 對比——Python 的 mutable 共享是預設行為:

data = [1, 2, 3]
a = data
b = data
a.append(4)
print(b)  # [1, 2, 3, 4]  —— 本來就是共享的

Rc 小結
#

特性 Rc<T>
所有權 共享(多個 Rc 擁有同一份資料)
執行緒安全 ❌(僅限單執行緒)
開銷 引用計數增減(很小)
用途 圖結構、樹的共享節點、觀察者模式

五、Arc<T>:跨執行緒的共享所有權
#

Arc<T>(Atomically Reference Counted)是 Rc<T>執行緒安全版本。它用 原子操作 來更新引用計數,所以可以安全地跨執行緒使用:

use std::sync::Arc;
use std::thread;

fn main() {
    let data = Arc::new(vec![1, 2, 3, 4, 5]);

    let mut handles = vec![];

    for i in 0..3 {
        let data_clone = Arc::clone(&data);
        let handle = thread::spawn(move || {
            let sum: i32 = data_clone.iter().sum();
            println!("執行緒 {} 計算的總和:{}", i, sum);
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }
}
// 三個執行緒安全地共享同一份 Vec!

Python 對比(用 threading):

import threading

data = [1, 2, 3, 4, 5]

def calc_sum(thread_id):
    print(f"執行緒 {thread_id} 計算的總和:{sum(data)}")

threads = [threading.Thread(target=calc_sum, args=(i,)) for i in range(3)]
for t in threads:
    t.start()
for t in threads:
    t.join()

Python 版本看起來更簡單,但它有 GIL 的限制。Rust 的 Arc 是真正的平行計算!

Arc + Mutex:可變的多執行緒共享
#

Rc + RefCell 類似,Arc + Mutex 讓你在多執行緒中安全地修改共享資料:

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter_clone = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter_clone.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("最終計數:{}", *counter.lock().unwrap());
    // 輸出:最終計數:10
}

Python 版本:

import threading

counter = 0
lock = threading.Lock()

def increment():
    global counter
    with lock:
        counter += 1

threads = [threading.Thread(target=increment) for _ in range(10)]
for t in threads:
    t.start()
for t in threads:
    t.join()

print(f"最終計數:{counter}")  # 10

Rc vs Arc 比較
#

特性 Rc<T> Arc<T>
執行緒安全
效能 較快(普通計數) 稍慢(原子操作)
用途 單執行緒共享 多執行緒共享
搭配可變性 RefCell<T> Mutex<T> / RwLock<T>

💡 如果你不需要跨執行緒,用 Rc 就好——它的效能比 Arc 好。

六、三大智慧指標完整比較
#

智慧指標 所有權 執行緒安全 可變性 Python 類比
Box<T> 獨佔 擁有者可改 所有 Python 物件
Rc<T> 共享 搭配 RefCell sys.getrefcount()
Arc<T> 共享 搭配 Mutex 多執行緒引用計數

什麼時候用哪個?
#

需要把值放 heap?
  └─ 是 → 只有一個所有者?
              └─ 是 → Box<T> ✅
              └─ 否 → 跨執行緒?
                          └─ 否 → Rc<T> ✅
                          └─ 是 → Arc<T> ✅

記憶體佈局圖解
#

Box<T>:
  Stack          Heap
  ┌─────┐      ┌──────┐
  │  b ──┼────→│ data │
  └─────┘      └──────┘
  (唯一所有者)

Rc<T> / Arc<T>:
  Stack          Heap
  ┌─────┐      ┌─────────────────┐
  │ a ──┼────→ │ count: 3        │
  ├─────┤  ┌──→│ data: [1, 2, 3] │
  │ b ──┼──┘   └─────────────────┘
  ├─────┤  ┌──→  (同一份資料)
  │ c ──┼──┘
  └─────┘

七、常見錯誤與陷阱
#

陷阱一:Rc 的循環引用
#

Rc 用引用計數管理記憶體,但如果 A 引用 B、B 引用 A,計數永遠不會歸零,造成 記憶體洩漏

use std::rc::Rc;
use std::cell::RefCell;

#[derive(Debug)]
struct Node {
    next: Option<Rc<RefCell<Node>>>,
}

fn main() {
    let a = Rc::new(RefCell::new(Node { next: None }));
    let b = Rc::new(RefCell::new(Node { next: Some(Rc::clone(&a)) }));

    // 製造循環引用!
    a.borrow_mut().next = Some(Rc::clone(&b));

    // 離開作用域時,a 和 b 的引用計數都是 2
    // 減 1 後還是 1 → 永遠不會釋放 → 記憶體洩漏!
    println!("a 的引用計數:{}", Rc::strong_count(&a)); // 2
    println!("b 的引用計數:{}", Rc::strong_count(&b)); // 2
}

解法:用 Weak<T>(弱引用)打破循環:

use std::rc::{Rc, Weak};
use std::cell::RefCell;

struct Node {
    next: Option<Weak<RefCell<Node>>>,  // 改用 Weak
}

fn main() {
    let a = Rc::new(RefCell::new(Node { next: None }));
    let b = Rc::new(RefCell::new(Node {
        next: Some(Rc::downgrade(&a)),  // 弱引用,不增加 strong count
    }));

    println!("a 的強引用計數:{}", Rc::strong_count(&a)); // 1
    println!("a 的弱引用計數:{}", Rc::weak_count(&a));   // 1
}

💡 Python 也有循環引用的問題!不過 Python 的垃圾回收器有 循環檢測gc 模組),所以通常不需要手動處理。Rust 沒有 GC,所以你必須自己注意。

陷阱二:Arc 的效能開銷
#

別因為「反正都能用」就到處用 Arc——原子操作比普通操作慢:

// ❌ 不必要的 Arc(單執行緒場景)
use std::sync::Arc;
let data = Arc::new(vec![1, 2, 3]);

// ✅ 單執行緒用 Rc 就好
use std::rc::Rc;
let data = Rc::new(vec![1, 2, 3]);

結語
#

恭喜你!學會了 Rust 的三大智慧指標 🎉

對 Python 工程師來說,這些概念可能一開始覺得繁瑣——「Python 根本不用管這些啊!」沒錯,但 Rust 的顯式記憶體管理正是它能做到 零成本抽象零 GC 暫停 的關鍵。

快速回顧:

  • Box<T>:簡單粗暴,把值丟到 heap,獨佔所有權
  • Rc<T>:單執行緒共享所有權,引用計數
  • Arc<T>:多執行緒共享所有權,原子引用計數
  • 需要可變性?配上 RefCell(單執行緒)或 Mutex(多執行緒)

下次我們會進入 Rust 的 async/await 世界,到時候 Arc 會大量出場——因為非同步任務天生就是多執行緒的場景!

拍拍君跟你一起加油 💪

延伸閱讀
#

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

相關文章

Rust Closures + Iterators:用 FP 風格寫出優雅的 Rust
·9 分鐘· loading · loading
Rust Python Closures Iterators Functional Programming
Rust Collections vs Python:Vec 和 HashMap 的生存指南
·7 分鐘· loading · loading
Rust Python Collections Vec Hashmap
Rust 生命週期(Lifetime)入門:編譯器教你管記憶體
·8 分鐘· loading · loading
Rust Lifetime Borrowing Memory-Safety Python
Rust 錯誤處理:Result/Option vs Python try/except
·7 分鐘· loading · loading
Rust Python Error-Handling Result Option
Rust for Python 開發者:Ownership 與 Borrowing 入門
·7 分鐘· loading · loading
Rust Python Ownership Borrowing Memory
Rust CLI 實戰:用 clap 打造命令列工具(Python Typer 對照版)
·5 分鐘· loading · loading
Rust Cli Clap Typer Python