Rust 智能指標 – Rc<T>(參考計數)
簡介
在 Rust 中,所有權(ownership)與借用(borrowing)是記憶體安全的核心機制。大多數情況下,我們會使用 Box<T>、&T、&mut T 來管理資料的生命週期。然而,當同一筆資料需要被多個所有者共享,而這些所有者之間又沒有明確的父子層級關係時,單純的所有權模型就會顯得笨拙。這時 Rc<T>(Reference Counted)就派上用場——它允許在單執行緒環境下,透過參考計數的方式安全地共享資料。
Rc<T> 不僅是學習 Rust 智能指標的重要一步,也是在實作圖形結構、樹狀資料或是事件系統時的常見工具。掌握它的使用方式、限制與最佳實踐,能讓你的程式碼既簡潔又安全。
核心概念
1. 什麼是 Rc<T>?
Rc<T> 是標準函式庫 std::rc 模組提供的智能指標。它在堆上分配一個 T,同時在內部維護一個 引用計數(reference count)。每當呼叫 clone() 時,計數會加一;當 Rc<T> 被 drop 時,計數會減一;當計數降至 0,堆上的資料才會被釋放。
注意:
Rc<T>僅適用於單執行緒(non‑thread‑safe)的情境。若需要跨執行緒共享,請改用Arc<T>(Atomic Reference Counted)。
2. 為什麼需要 clone() 而不是直接賦值?
在 Rust 中,賦值會觸發 所有權轉移(move)。Rc<T> 為了保留多個所有者,實作了 Clone trait,讓開發者顯式地「複製」指標(其實只複製指標本身,資料仍只佔一份)。這樣的設計避免了不小心產生 資料競爭(data race)。
3. Rc<T> 與 RefCell<T> 的組合
Rc<T> 本身只能提供 不可變的共享(&T),如果你需要在多個所有者之間可變地操作資料,必須再加上一層 內部可變性(interior mutability),最常見的組合是 Rc<RefCell<T>>。RefCell<T> 在執行時執行借檢查,允許在單執行緒內部取得可變或不可變的借用。
程式碼範例
以下示範 5 個實用例子,從最基本的 Rc 使用,到 Rc<RefCell> 的進階應用。
1. 基本的 Rc 建立與 clone
use std::rc::Rc;
fn main() {
// 建立一個 Rc 包住字串
let rc_hello = Rc::new(String::from("Hello, Rust!"));
// 取得目前的引用計數(此時為 1)
println!("count = {}", Rc::strong_count(&rc_hello)); // => 1
// 複製 rc_hello,計數會加一
let rc_clone = rc_hello.clone();
println!("count after clone = {}", Rc::strong_count(&rc_hello)); // => 2
// rc_clone 離開作用域,計數減一
} // 這裡 rc_hello 也會被 drop,計數回到 0,資料釋放
重點:使用
Rc::strong_count可以在除錯時快速檢查計數是否符合預期。
2. 多個所有者共享同一筆資料
use std::rc::Rc;
fn print_name(name: Rc<String>) {
println!("Name: {}", name);
}
fn main() {
let name = Rc::new(String::from("Alice"));
// 兩個函式同時持有所有權
print_name(name.clone());
print_name(name.clone());
// 此時仍可在 main 中使用 name
println!("Still accessible: {}", name);
}
說明:每次傳入
print_name時,我們都clone()產生新的Rc,讓函式取得自己的所有者。
3. Rc 與 RefCell 結合:可變共享
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
// 使用 RefCell 包住可變的 Vec
let shared_vec = Rc::new(RefCell::new(vec![1, 2, 3]));
// 第一個所有者 push 一個元素
{
let mut v = shared_vec.borrow_mut(); // 可變借用
v.push(4);
} // 借用在此結束
// 第二個所有者讀取內容
{
let v = shared_vec.borrow(); // 不可變借用
println!("Vec content: {:?}", *v); // => [1, 2, 3, 4]
}
}
提醒:
RefCell在執行時會檢查借用規則,若同時取得兩個可變借用會 panic。
4. 建立樹狀結構(父子關係)— Rc + RefCell
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
children: RefCell::new(vec![]),
});
let root = Rc::new(Node {
value: 1,
children: RefCell::new(vec![leaf.clone()]),
});
// 再加入另一個子節點
let child = Rc::new(Node {
value: 2,
children: RefCell::new(vec![]),
});
root.children.borrow_mut().push(child.clone());
println!("Root: {:?}", root);
println!("Leaf count: {}", Rc::strong_count(&leaf)); // 2 (leaf + root.children)
}
重點:使用
Rc可以讓多個父節點同時持有同一子節點的所有權,形成「共享」的樹狀結構。
5. 循環引用的問題與 Weak<T> 的解決方式(簡介)
use std::rc::{Rc, Weak};
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>, // 使用 Weak 打破循環
children: RefCell<Vec<Rc<Node>>>,
}
fn main() {
let child = Rc::new(Node {
value: 2,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
let parent = Rc::new(Node {
value: 1,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![child.clone()]),
});
// 設定 child 的 parent 為 parent(弱引用)
*child.parent.borrow_mut() = Rc::downgrade(&parent);
// 循環引用已被打破,兩個 Rc 都能正確釋放
}
說明:雖然這段程式碼屬於
Weak<T>的範例,但它說明了Rc<T>若直接相互持有會造成記憶體泄漏,因此在需要「父」指向「子」的同時,父指向子應使用Weak<T>。
常見陷阱與最佳實踐
| 陷阱 | 可能的後果 | 解決或避免方式 |
|---|---|---|
循環引用(兩個 Rc 互相持有) |
記憶體永遠不會被釋放(leak) | 使用 Weak<T> 斷開循環,或重新設計資料結構 |
在多執行緒中使用 Rc<T> |
編譯錯誤(Rc<T> 不是 Send) |
改用 Arc<T>(原子參考計數) |
過度使用 Rc<RefCell<T>> |
失去編譯期的借用檢查,執行時才 panic | 盡量在設計階段使用所有權或 enum 表達變化,僅在必要時才使用 RefCell |
忘記 clone() |
所有權被移走,導致變數無法再使用 | 明確呼叫 clone(),或使用 let rc2 = Rc::clone(&rc1);(語意更清晰) |
不必要的 Rc |
增加引用計數開銷、破壞簡潔性 | 若只有單一所有者,直接使用 Box<T> 或 &T |
最佳實踐:
- 先考慮所有權:只有在確定需要多個所有者時才引入
Rc。 - 最小化
RefCell的範圍:將可變共享限制在最小的程式區塊,減少 panic 風險。 - 使用
Rc::strong_count監控計數:在除錯階段確認沒有意外的引用遺留。 - 對於父子關係,使用
Weak:父節點持有子節點的Rc,子節點持有父節點的Weak。 - 文件化所有者關係:在程式碼註解或 README 中說明哪個模組/結構負責持有
Rc,方便日後維護。
實際應用場景
抽象語法樹(AST)
編譯器或解譯器常需要在多個階段(解析、分析、轉譯)共享同一棵語法樹。使用Rc<Node>可以讓不同階段的演算法同時持有節點,而不必擔心所有權衝突。GUI 元件樹
UI 框架(如druid、iced)的視圖層級結構通常是樹狀的,子視圖需要被父視圖持有,同時事件處理器可能需要臨時持有子視圖的引用。Rc<RefCell<Widget>>能提供靈活的共享與可變更新。資源緩存(Cache)
圖片、音效或資料庫連線等資源在多個模組中被重複使用。將資源包成Rc<T>放入全域緩存表,使用者只要clone()即可取得共享引用,資源會在最後一個使用者離開時自動釋放。事件訂閱系統
多個觀察者(observer)需要同時持有同一個事件來源(subject)。Rc<RefCell<Subject>>讓觀察者在需要時取得可變的訂閱列表,並在解除訂閱時自動更新計數。遊戲物件關係
在 2D/3D 遊戲中,實體(entity)可能同時被多個系統(渲染、物理、AI)引用。Rc<Component>能讓各系統共享同一個組件,而不必手動管理生命週期。
總結
Rc<T> 是 Rust 在單執行緒環境下提供的 參考計數智能指標,讓多個所有者可以安全地共享同一筆資料。透過 clone() 取得新所有者、Rc::strong_count 監控計數,以及與 RefCell<T>、Weak<T> 的組合,我們可以構建彈性十足且記憶體安全的資料結構。使用時務必注意 循環引用、跨執行緒的限制,以及 過度依賴內部可變性 所帶來的執行時風險,遵循「先考慮所有權、後使用 Rc」的原則,才能寫出既高效又可靠的 Rust 程式。
祝你在 Rust 的旅程中玩得開心,寫出更安全、更優雅的程式碼! 🚀