一、前言 #
在 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?幾個常見情境:
- 資料太大,不適合放 stack
- 大小在編譯期未知(遞迴型別、動態大小)
- 需要共享所有權(多個地方同時擁有同一份資料)
- 需要讓資料活得比當前作用域更久
三、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> 有兩個重要限制:
- 只限單執行緒——不能跨執行緒傳遞
- 預設不可變——如果需要修改共享資料,要搭配
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 會大量出場——因為非同步任務天生就是多執行緒的場景!
拍拍君跟你一起加油 💪