Rc<T>
本集目標
學會用 Rc<T> 讓多個擁有者共享同一份資料,理解參考計數的原理。
概念說明
上一集學了 Box<T>——一個保險箱只有一把鑰匙,一個擁有者。但有時候你需要多個擁有者共享同一份資料。
問題:一個值只能有一個擁有者
fn main() {
let a = Box::new(String::from("hello"));
let b = a; // move!a 不能再用了
}
如果你希望 a 和 b 都能用這個值,怎麼辦?
你可能會想:「那 clone 一份不就好了?」
fn main() {
let a = Box::new(String::from("hello"));
let b = a.clone(); // 複製了整個 String 的內容
}
這確實能讓 a 和 b 都能用。但問題是——clone 是真的把 heap 上的資料完整複製了一份。如果資料很大(比如一個很長的 Vec),每次 clone 都是一筆不小的開銷。而且 a 和 b 指向的是兩份獨立的資料,改了 a 不會影響 b。
如果你需要的是「多個人共享同一份資料」,Box 的 clone 就不是正確的工具了。
Rc:參考計數
還記得第四章的保險箱比喻嗎?Rust 預設的規則是「一個保險箱只有一把鑰匙」——這就是所有權。但 Rc<T> 打破了這個預設:它讓你配好幾把鑰匙,都能打開同一個保險箱。
Rc<T>(reference counting)用一個「計數器」來追蹤目前有幾把鑰匙:
- 建立
Rc時,計數 = 1 .clone()時,計數 +1(不會複製資料,只是增加計數)- 某個
Rc離開作用域時,計數 -1 - 計數歸零時,資料才會被真正釋放
Rc 的 .clone() 不是深度複製
對 Rc 呼叫 .clone() 和我們之前學的 .clone() 不一樣——它不會複製裡面的資料,只是增加參考計數。所以速度很快,成本很低。
Rc 的 clone 不是用 #[derive(Clone)] 產生的,而是標準庫自己手動實作的。如果用 derive,它會深度複製裡面的資料;但 Rc 的 clone 只是增加計數器,行為完全不同。
Rc 是唯讀的
Rc<T> 只提供不可變的存取——所有擁有者都只能讀,不能改。如果需要可以改,之後會學 RefCell。
等等,第四章不是說多把鑰匙會有問題嗎?
第四章說一個保險箱只有一把鑰匙。那 Rc 怎麼能配好幾把?有兩件事要知道:
- 計數器的代價:
Rc內部有一個計數器來追蹤目前有幾把鑰匙,這樣才知道什麼時候該銷毀保險箱。這個計數器在每次clone和drop時都要更新,是Box沒有的額外開銷。Box保證只有一把鑰匙,離開作用域就銷毀,不需要計數——Rc並不是像Box那樣基本的操作,而是用額外的機制換來「多個擁有者」的能力 - 使用上的限制:未來講到多執行緒的時候會提到,
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,只增加計數,不複製內部資料——成本很低- 某個
Rc被drop時計數 -1,歸零時才釋放資料 Rc是唯讀的——所有擁有者只能讀,不能改- Rust 原生的「一個擁有者」是零成本的語言保證;
Rc的「多個擁有者」是用計數器在執行時期模擬出來的,有額外開銷和限制 - 用
Rc::strong_count(&x)查看目前的參考計數