Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rc<T>

本集目標

學會用 Rc<T> 讓多個擁有者共享同一份資料,理解參考計數的原理。

概念說明

上一集學了 Box<T>——一個保險箱只有一把鑰匙,一個擁有者。但有時候你需要多個擁有者共享同一份資料。

問題:一個值只能有一個擁有者

fn main() {
    let a = Box::new(String::from("hello"));
    let b = a; // move!a 不能再用了
}

如果你希望 ab 都能用這個值,怎麼辦?

你可能會想:「那 clone 一份不就好了?」

fn main() {
    let a = Box::new(String::from("hello"));
    let b = a.clone(); // 複製了整個 String 的內容
}

這確實能讓 ab 都能用。但問題是——clone 是真的把 heap 上的資料完整複製了一份。如果資料很大(比如一個很長的 Vec),每次 clone 都是一筆不小的開銷。而且 ab 指向的是兩份獨立的資料,改了 a 不會影響 b

如果你需要的是「多個人共享同一份資料」,Boxclone 就不是正確的工具了。

Rc:參考計數

還記得第四章的保險箱比喻嗎?Rust 預設的規則是「一個保險箱只有一把鑰匙」——這就是所有權。但 Rc<T> 打破了這個預設:它讓你配好幾把鑰匙,都能打開同一個保險箱

Rc<T>(reference counting)用一個「計數器」來追蹤目前有幾把鑰匙:

  • 建立 Rc 時,計數 = 1
  • .clone() 時,計數 +1(不會複製資料,只是增加計數)
  • 某個 Rc 離開作用域時,計數 -1
  • 計數歸零時,資料才會被真正釋放

Rc.clone() 不是深度複製

Rc 呼叫 .clone() 和我們之前學的 .clone() 不一樣——它不會複製裡面的資料,只是增加參考計數。所以速度很快,成本很低。

Rcclone 不是用 #[derive(Clone)] 產生的,而是標準庫自己手動實作的。如果用 derive,它會深度複製裡面的資料;但 Rcclone 只是增加計數器,行為完全不同。

Rc 是唯讀的

Rc<T> 只提供不可變的存取——所有擁有者都只能讀,不能改。如果需要可以改,之後會學 RefCell

等等,第四章不是說多把鑰匙會有問題嗎?

第四章說一個保險箱只有一把鑰匙。那 Rc 怎麼能配好幾把?有兩件事要知道:

  1. 計數器的代價Rc 內部有一個計數器來追蹤目前有幾把鑰匙,這樣才知道什麼時候該銷毀保險箱。這個計數器在每次 clonedrop 時都要更新,是 Box 沒有的額外開銷。Box 保證只有一把鑰匙,離開作用域就銷毀,不需要計數——Rc 並不是像 Box 那樣基本的操作,而是用額外的機制換來「多個擁有者」的能力
  2. 使用上的限制:未來講到多執行緒的時候會提到,Rc 事實上也無法在某些情況下避免資料競爭,因此有使用上的限制。這裡先知道就好

換句話說,Rust 原生的所有權規則就是「一把鑰匙」——這是語言層面的保證,零成本。Rc 的「多把鑰匙」是用計數器在執行時期模擬出來的,繞過了原生的限制,但也因此有額外的成本和限制。能用 Box 就用 Box,需要共享時才用 Rc

範例程式碼

use std::rc::Rc;

fn main() {
    // 建立 Rc,計數 = 1
    let a = Rc::new(String::from("共享的資料"));
    println!("建立 a,計數 = {}", Rc::strong_count(&a));

    // clone 只是增加計數,不複製資料
    let b = a.clone();
    println!("clone 給 b,計數 = {}", Rc::strong_count(&a));

    let c = a.clone();
    println!("clone 給 c,計數 = {}", Rc::strong_count(&a));

    // a, b, c 都可以讀取
    println!("a = {}", a);
    println!("b = {}", b);
    println!("c = {}", c);

    {
        let d = a.clone();
        println!("在作用域裡,計數 = {}", Rc::strong_count(&a));
    } // d 被 drop,計數 -1
    println!("離開作用域後,計數 = {}", Rc::strong_count(&a));

    // 實際用途:多個 struct 共享同一份資料
    let shared_name = Rc::new(String::from("Rust"));

    let greeting1 = shared_name.clone();
    let greeting2 = shared_name.clone();

    println!("1: {}", greeting1);
    println!("2: {}", greeting2);
}

重點整理

  • Rc<T> 用參考計數讓多個擁有者共享同一份資料
  • Rc::new(value) 建立時計數為 1
  • .clone() 計數 +1,只增加計數,不複製內部資料——成本很低
  • 某個 Rcdrop 時計數 -1,歸零時才釋放資料
  • Rc唯讀的——所有擁有者只能讀,不能改
  • Rust 原生的「一個擁有者」是零成本的語言保證;Rc 的「多個擁有者」是用計數器在執行時期模擬出來的,有額外開銷和限制
  • Rc::strong_count(&x) 查看目前的參考計數