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 再也不能用它。
如果你希望 a 和 b 都能繼續使用同一段文字,怎麼辦?
你可能會想:「呼叫 .clone() 不就好了?」
fn main() {
let a = Box::new(String::from("hello"));
let b = a.clone(); // 建立一個新的 Box,裝著另一個 String
}
這確實能讓 a 和 b 都能用。但在這個 Box<String> 的例子裡,.clone() 建立的是另一個 Box,裝著一個內容相同的新 String。如果內部的值很大,這會是一筆不小的開銷。而且 a 和 b 現在用的是兩個各自獨立的 String 值,不是同一個。
如果你需要的是「好幾個值使用同一份 heap 資料」,Box 的 .clone() 就不是正確的工具。
Rc 的 .clone():真的只多打一把鑰匙
還記得第 4 章對 .clone() 的定義嗎?——「弄一個跟原本一樣好用的新鑰匙圈,並確保這樣做不會出問題」。對 String 或 Vec<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 資料才會被釋放。
計數器管的是「保險箱什麼時候可以撤掉」:只要還有人手上有鑰匙,保險箱就不會被撤走,所以不會有人拿著鑰匙、卻發現保險箱早就不在了。
這也表示 Rc 的 Clone 不能用 derive。Rc 的 .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 把那個情境裡的兩種危險分別堵住了:
- 沒有人能動保險箱裡的東西:
Rc<T>只給共享的讀取。第 4 章怕的是有人整理、有人拿走;大家都只能看的話,這件事就不會發生。 - 保險箱不會在還有人拿著鑰匙的時候被撤掉:這是計數器的工作。要等最後一把鑰匙消失,資料才會被釋放。
第二點不是免費的:計數器在每次 clone 和 drop 時都要更新,這是 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)查看目前的參考計數。