Rust 的借用檢查器(borrow checker)是個好東西,它在編譯期就幹掉了整整一大類的 bug,但是現實是殘酷的。
有時候,為了榨出最後一點效能,比如實現一個快取,我們需要在一個「看起來」不可變的結構內部,修改某些資料。
這就是 RefCell
這種東西存在的地方。
它不是一個優雅的設計模式,它是一個後門,一個妥協。
Cell<T>
:值語義的複製進出;RefCell<T>
:執行期動態借用(違規就 panic)。
這個問題很常見。
假如有個結構,有個方法應該只是讀取資訊,但你想在內部偷偷快取結果。
struct Report {
// ... 大量資料
cache: Option<String>,
}
impl Report {
// 從外部看,這就是個讀取操作,所以簽名是 &self
fn get_summary(&self) -> String {
if let Some(cached) = &self.cache {
return cached.clone();
}
let summary = self.compute_summary(); // 昂貴的計算
self.cache = Some(summary.clone()); // ❌ 編譯器報錯。你違背了 &self 的承諾。
summary
}
fn compute_summary(&self) -> String { /* ... */ "summary".into() }
}
編譯器阻止了你,這是它的職責。
在99%的情況下,你應該聽它的,回去修改你的設計。但如果,且只有如果,效能數據證明這個快取至關重要,你就需要一個方法告訴編譯器:「好了,我知道我在做什麼。」
Cell
與 RefCell
這兩個東西讓你可以在執行期繞過編譯器的靜態檢查。
它們都只用於單執行緒。
Cell<T>
:用於可 Copy
的型別 (如 i32
) 這東西很直接。它就是一個值容器,你可以直接用 set()
換掉裡面的值,用 get()
拿一份拷貝出來。沒有借用,沒有恐慌,風險很低。能用它就用它。
RefCell<T>
:用於無法 Copy
的型別 (如 String
) 這是我們要重點關注的危險品。它做的事情很簡單:把借用規則的檢查,從編譯期推遲到執行期。
.borrow()
:在執行期取得一個共享參考 (&T
)。
.borrow_mut()
:在執行期取得一個獨佔參考 (&mut T
)。
代價是什麼?如果你在執行時違反了借用規則(比如,在一個 .borrow_mut()
的生命週期內又來了一次 .borrow()
),你的程式會直接崩潰 (panic
)。沒有商量的餘地。
核心差異:Cell
是低成本的位元交換。RefCell
則是一場豪賭,賭你在執行期的邏輯永遠不會違背借用規則。賭輸的代價是整個程式掛掉。
使用 RefCell
的唯一準則,就是把它當成核廢料處理:用最厚的牆把它封起來,並標上警告標誌。
&T
/&mut T
暴露安全界面。use std::cell::RefCell;
struct Report {
// ...
cache: RefCell<Option<String>>, // 把不穩定因素關進 RefCell
}
impl Report {
fn get_summary(&self) -> String {
// 請求讀取權限
if let Some(cached) = self.cache.borrow().as_ref() {
return cached.clone();
}
let summary = self.compute_summary();
// 請求寫入權限,用完立刻釋放
*self.cache.borrow_mut() = Some(summary.clone());
summary
}
fn compute_summary(&self) -> String { /* ... */ "summary".into() }
}
get_summary
對外的 &self
簽名是個謊言。
任何內部使用了 RefCell
並進行寫入操作的函式,即使其簽名是 &self
,在程式碼審查(Code Review)中也必須被當成一個 &mut self
的可變函式來同等對待。
審查者必須假設它有副作用,並以最嚴格的標準去檢查所有可能的借用衝突。
它告訴呼叫者「這很安全」,但其內部正在處理隨時可能引爆的炸藥。
這種封裝的目的不是為了「優雅」,而是為了損害控制——確保爆炸只發生在這個函式內部,不會洩漏出去。
RefCell
當萬能解鎖器 → 真問題是資料結構設計錯。RefCell<T>
本身 → 把風險外送給使用者。RefCell
→ 執行期踩雷,應改用 Mutex/RwLock
。RefCell
之前先問自己: 能不能透過重構,或改變資料所有權來避免內部可變性?
如果不行,且是單執行緒,再考慮 Cell
(用於 Copy
型別) 或 RefCell
(用於非 Copy
型別)。
拿出數據證明。 這個快取到底有多重要?是讓程式快了 1 奈秒還是 10 秒?如果沒有 benchmark 數據來證明這 hack 是絕對、非做不可的,就不需要考慮 RefCell
。
如果非用不可…… 如果通過了以上所有考驗,確定這是唯一解法的話。
RefCell
不會讓你成為一個更好的工程師。
**知道何時絕對不要用它,才會。