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> 值共享同一份 heap 資料,並理解參考計數如何讓這份資料活著。

概念說明

上一集是 Box<T>:一個保險箱,一把鑰匙。Box 值就是那把鑰匙,heap 資料則是保險箱裡的東西。

但有時候,程式的好幾個地方需要使用同一份 heap 資料,而不是另外建立一份獨立的 heap 值。

問題:move 一個 Box

Box<T> 只有一把鑰匙。把它指定給另一個變數,就是把鑰匙 move 過去:

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

move 之後,鑰匙在 b 手上。heap 資料沒有被複製,而 a 再也不能用它。

如果你希望 ab 都能繼續使用同一段文字,怎麼辦?

你可能會想:「呼叫 .clone() 不就好了?」

fn main() {
    let a = Box::new(String::from("hello"));
    let b = a.clone(); // 建立一個新的 Box,裝著另一個 String
}

這確實能讓 ab 都能用。但在這個 Box<String> 的例子裡,.clone() 建立的是另一個 Box,裝著一個內容相同的新 String。如果內部的值很大,這會是一筆不小的開銷。而且 ab 現在用的是兩個各自獨立的 String,不是同一個。

如果你需要的是「好幾個值使用同一份 heap 資料」,Box.clone() 就不是正確的工具。

Rc.clone():真的只多打一把鑰匙

還記得第 4 章對 .clone() 的定義嗎?——「弄一個跟原本一樣好用的新鑰匙圈,並確保這樣做不會出問題」。對 StringVec<i32> 這類單純的具所有權型別來說,「確保」的方法是打一把新鑰匙、配一個內容相同的新保險箱。

Rc 就是第 4 章預告過的例外:它的 .clone() 真的只多打一把鑰匙。你得到一個新的 Rc 值,開的是同一個保險箱。資料本身不會被複製,這就是為什麼就算資料很大,對 Rc 呼叫 .clone() 依然便宜。

但如果好幾把鑰匙都能開同一個保險箱,Rust 就需要一個辦法知道保險箱什麼時候可以撤掉。這就輪到計數器登場了。

Rc:參考計數

Rc<T>reference counting(參考計數)的縮寫。

Rc<T> 想成一個允許多把鑰匙的保險箱。Rc 替這個保險箱維護一個計數器。

計數器追蹤的是目前還有幾個 Rc<T> 值能開這個保險箱:

  • 建立第一個 Rc:計數 = 1。
  • .clone():計數 +1——你得到的是同一個保險箱的另一把鑰匙,不是另一份獨立的資料。
  • 某個 Rc 值離開作用域:計數 -1。
  • 計數歸零時,heap 資料才會被釋放。

計數器管的是「保險箱什麼時候可以撤掉」:只要還有人手上有鑰匙,保險箱就不會被撤走,所以不會有人拿著鑰匙、卻發現保險箱早就不在了。

這也表示 RcClone 不能用 deriveRc.clone() 要做兩件事:多打一把鑰匙,還要把保險箱上的計數器加一。derive 只會對每個欄位各自呼叫 .clone(),第一件做得到,第二件做不到——鑰匙多了一把,計數器卻沒有跟著加。所以標準庫替 Rc 手寫了 Clone

要分清楚的兩個層次

這個區分很重要:

use std::rc::Rc;

fn main() {
    let a = Rc::new(String::from("hello"));
    let b = a.clone();
}

這裡有兩個層次:

  • a 擁有一個 Rc<String> 值。
  • b 擁有另一個 Rc<String> 值。
  • 兩個 Rc<String> 值開的是同一份 heap 資料。
  • 計數器追蹤的是目前還存在幾個 Rc<String> 值。

所以 Rc 並不代表一般的 Rust 值就不守所有權規則了。每個 Rc 值仍然是一個普通的值:它會 move、會被 drop、move 之後舊變數就不能再用。特別的是那些 Rc 值背後的 heap 資料:它會一直活到最後一把鑰匙消失為止。

Rc 是唯讀的

Rc<T> 本身提供的是共享的讀取。好幾個 Rc 值可以讀同一份 heap 資料,但不能隨意修改它。

如果需要「共享而且可以修改」的資料,之後會學 RefCell<T>

等等,第 4 章不是提醒過不能隨便複製鑰匙嗎?

第 4 章的擔心很具體:兩個人各拿一把鑰匙開同一個保險箱,A 正在整理保險箱裡的東西,B 同時把東西拿走了。那 Rc 憑什麼可以配好幾把鑰匙?

因為 Rc 把那個情境裡的兩種危險分別堵住了:

  1. 沒有人能動保險箱裡的東西Rc<T> 只給共享的讀取。第 4 章怕的是有人整理、有人拿走;大家都只能看的話,這件事就不會發生。
  2. 保險箱不會在還有人拿著鑰匙的時候被撤掉:這是計數器的工作。要等最後一把鑰匙消失,資料才會被釋放。

第二點不是免費的:計數器在每次 clonedrop 時都要更新,這是 Box<T> 沒有的額外開銷。Box<T> 是單純的「一把鑰匙」heap 配置,沒有計數器;Rc<T> 加上了執行時期的記帳,讓好幾個 Rc 值能開同一個保險箱。這種共享很有用,但不是免費的。

另外,講到多執行緒的時候,我們會看到 Rc 在那裡還有別的限制。這裡先知道就好。

範例程式碼

use std::rc::Rc;

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

    // .clone() 替同一份 heap 資料多建立一個 Rc
    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));

    // 實際用途:好幾個值共享同一個名字
    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 值共享同一份 heap 資料。
  • Rc::new(value) 建立時計數為 1。
  • .clone() 替同一份 heap 資料多建立一個 Rc 值:計數 +1,不會多出一份獨立的內部值。
  • drop 掉一個 Rc 計數 -1;計數歸零時 heap 資料才被釋放。
  • Rc<T> 本身提供共享的讀取,不是不受限制的修改。
  • Rc 能安全共享靠兩件事:只給讀取,加上計數器保證資料活到最後一個 Rc 消失。
  • Rc<T> 有使用上的限制,不是所有共享問題的萬用解。
  • 每個 Rc 值仍然遵守一般的所有權規則:它像其他非 Copy 值一樣 move、一樣 drop
  • Rc::strong_count(&x) 查看目前的參考計數。