內部可變性(Cell 與 RefCell)
別名與可變互斥規則說你只能透過一個獨一的 &mut 來改動一個值。但真實的程式有時真的需要透過一個共享參考來更改某物——一張圖裡被其他好幾個節點指著的節點、藏在一個原本唯讀物件裡的快取、一個增加自身計數的參考計數值。編譯器無法在編譯期證明這些安全,但只要仔細檢查,它們「就是」安全的。內部可變性就是允許這件事的模式:特殊的包裝型別,讓你即使只持有對包裝器的 &T,也能改動裡面的值,把借用檢查從編譯期搬到執行期(或搬到硬體)。
兩個單執行緒工具是 Cell 與 RefCell。一個 Cell<T> 持有一個你可以整個替換掉的值:你無法取得指向它內容的參考,但你可以 .set(new) 或 .get()(對 Copy 型別)或 .replace(new),就借用規則而言是整個值原子地換掉——從不存在指向它的參考,所以沒有東西可被別名。一個 RefCell<T> 更豐富:它讓你借用內層的值,.borrow() 給你一個共享守衛、.borrow_mut() 給你一個獨占守衛,並執行「許多讀取者或一個寫入者」這條編譯器平常檢查的「同一條」規則——但在執行期,靠內部保持一個小小的借用計數器。若你在一個 .borrow() 仍存活時呼叫 .borrow_mut(),RefCell 不會無聲地損毀;它會以「already borrowed」恐慌(panic)。所以 RefCell 並不移除規則;它把檢查從編譯器搬到一個執行期計數器,用一個編譯錯誤換來一次可能的恐慌。這筆交換替你換來靜態檢查器無法允許的彈性。
為何重要:內部可變性正是你建構那些借用檢查器原本會禁止的資料結構的方式——共享圖、帶父指標的樹、觀察者模式——而不必你自己墜入 unsafe(這些型別內部使用 unsafe,已被審核過一次,所以你不必)。誠實的代價是真實的:RefCell 的執行期檢查若你的借用錯了會恐慌,而一個靜態錯誤永遠到不了正式環境;而且 Cell/RefCell 只限單執行緒(兩者都不是 Sync),所以對「跨」執行緒的共享改動,你需要那組同步家族——Mutex 與 RwLock(它們阻塞其他執行緒而非恐慌)以及 Atomic 型別(對單一值無鎖)。拇指法則是:優先用編譯器的靜態檢查;當一個正當的模式需要單執行緒上的共享改動時採用 Cell/RefCell;當它跨執行緒時採用 Mutex/RwLock/Atomic。
use std::cell::RefCell; let log = RefCell::new(Vec::new()); log.borrow_mut().push("first"); // 透過共享 & 改動,於執行期檢查 log.borrow_mut().push("second"); println!("{:?}", log.borrow()); // ["first", "second"] // 違反規則會恐慌而非損毀: let a = log.borrow(); // 共享借用存活中…… // let b = log.borrow_mut(); // 恐慌:already borrowed: BorrowMutError
RefCell 保留同一條「許多讀取者或一個寫入者」規則,但在執行期檢查,違反時恐慌而非拒絕編譯。
內部可變性不是繞過借用規則——它是把檢查搬位置。RefCell 可能在執行期恐慌,那是編譯器原本會報錯之處,而且 Cell/RefCell 只限單執行緒;跨執行緒要改用 Mutex、RwLock 或 Atomic。