本文 AI 產出,尚未審核

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. RcRefCell 結合:可變共享

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

最佳實踐

  1. 先考慮所有權:只有在確定需要多個所有者時才引入 Rc
  2. 最小化 RefCell 的範圍:將可變共享限制在最小的程式區塊,減少 panic 風險。
  3. 使用 Rc::strong_count 監控計數:在除錯階段確認沒有意外的引用遺留。
  4. 對於父子關係,使用 Weak:父節點持有子節點的 Rc,子節點持有父節點的 Weak
  5. 文件化所有者關係:在程式碼註解或 README 中說明哪個模組/結構負責持有 Rc,方便日後維護。

實際應用場景

  1. 抽象語法樹(AST)
    編譯器或解譯器常需要在多個階段(解析、分析、轉譯)共享同一棵語法樹。使用 Rc<Node> 可以讓不同階段的演算法同時持有節點,而不必擔心所有權衝突。

  2. GUI 元件樹
    UI 框架(如 druidiced)的視圖層級結構通常是樹狀的,子視圖需要被父視圖持有,同時事件處理器可能需要臨時持有子視圖的引用。Rc<RefCell<Widget>> 能提供靈活的共享與可變更新。

  3. 資源緩存(Cache)
    圖片、音效或資料庫連線等資源在多個模組中被重複使用。將資源包成 Rc<T> 放入全域緩存表,使用者只要 clone() 即可取得共享引用,資源會在最後一個使用者離開時自動釋放。

  4. 事件訂閱系統
    多個觀察者(observer)需要同時持有同一個事件來源(subject)。Rc<RefCell<Subject>> 讓觀察者在需要時取得可變的訂閱列表,並在解除訂閱時自動更新計數。

  5. 遊戲物件關係
    在 2D/3D 遊戲中,實體(entity)可能同時被多個系統(渲染、物理、AI)引用。Rc<Component> 能讓各系統共享同一個組件,而不必手動管理生命週期。


總結

Rc<T> 是 Rust 在單執行緒環境下提供的 參考計數智能指標,讓多個所有者可以安全地共享同一筆資料。透過 clone() 取得新所有者、Rc::strong_count 監控計數,以及與 RefCell<T>Weak<T> 的組合,我們可以構建彈性十足且記憶體安全的資料結構。使用時務必注意 循環引用跨執行緒的限制,以及 過度依賴內部可變性 所帶來的執行時風險,遵循「先考慮所有權、後使用 Rc」的原則,才能寫出既高效可靠的 Rust 程式。

祝你在 Rust 的旅程中玩得開心,寫出更安全、更優雅的程式碼! 🚀