Rust 的借用檢查零成本,卻不准兩個 &mut 指向同一份資料;
Rc<RefCell<T>> 換來共享可變,代價是 .borrow_mut()
可能在執行期直接 panic。Ante 想證明這不是二選一——它讓共享的可變物件照樣能借出
欄位,且不付 RefCell 的成本、也不冒它的崩潰風險。
Ante:把借用檢查和引用計數揉在一起
同一個需求擺在三種記憶體模型面前:你有一艘
Spaceship,好幾個地方都握著它的引用,而你現在想改它的一個欄位。
純借用檢查(Rust 的單一所有權加 &mut)會告訴你這辦不到,因為
「In Rust, we can't have multiple &mut references pointing to the same data.」;
純引用計數(Rc<RefCell<Spaceship>>)辦得到,但把靜態的
別名檢查推遲到執行期,握錯了就 panic。Evan Ovadia 在 6 月 28 日的文章裡介紹
Jake 的 Ante 語言想走第三條路:預設引用計數、共享可變,卻又保留借用檢查等級的
保證,「without run-time panics or overhead that come from Rust's RefCell or
Swift's exclusivity checking.」
這是一個典型的三方權衡,不是誰全面勝出的問題。四個判準決定了你在什麼情境下該用哪一個: 別名規則(誰能同時握住兩個可變引用)、執行期成本(檢查發生在編譯期還是執行期)、 崩潰風險(別名衝突會不會變成 runtime panic)、以及人體工學(寫起來順不順、API 穩不穩)。 先把三種方案在這幾格上的落點擺出來,後面每一節再逐軸拆開機制與代價。
點欄位標題可重排 · 3 種方案 × 7 個判準
| 判準 | 純借用檢查(Rust &mut) | 純引用計數(Rc<RefCell>) | Ante 混合 |
|---|---|---|---|
| 共享可變別名 | 禁止(不能有兩個 &mut) | 允許(多個持有者共享) | 允許(預設共享可變) |
| 別名檢查發生在 | 編譯期 | 執行期(borrow flag) | 編譯期(scope 分析) |
| 執行期成本 | 零 | RefCell borrow 計數 + RC 計數 | RC 計數,無 RefCell 檢查 |
| 別名衝突後果 | 編譯不過 | 執行期 panic | 編譯不過 |
| 改共享物件的欄位 | 要重構所有權 | 先 borrow_mut 再改 | 直接借欄位(shape-stable) |
| 改 union / enum 的 variant | 可(單一所有權下) | 先 borrow_mut 再 match | 共享時不允許借 variant |
| 主要脆弱面 | 共享可變寫不出來 | hold 錯就崩 | 加欄位可能是破壞性改動 |
表只是把答案先攤開。真正要理解的是為什麼 Ante 能同時佔住兩個看似矛盾的格子。 它的核心工具有兩個:一個叫 shape-stability 的靜態不變式,決定了哪些引用「永遠有效」; 以及一個把共享可變引用「暫時」轉成獨佔引用的規則。下面這個互動圖把三種方案放進同一組 別名情境裡——點選一個情境,看三方各自的判決。
點任一情境看三方判決 · 4 種別名情境 × 3 種方案
情境① · 兩處同時要可變
情境② · 借共享物件的欄位
情境③ · 暫時獨佔
情境④ · 借共享 union 的 variant
別名規則:誰能同時握住兩個可變引用
別名規則是這三個模型的分水嶺。Rust 的立場最硬:一份資料在任一時刻,最多只能有一個
&mut。文章直接引用這條規則——「In Rust, we can't have multiple
&mut references pointing to the same data.」——它是 Rust 記憶體安全的基石,
也是借用檢查零成本的原因:既然編譯器已經證明沒有共享可變,執行期就不需要任何檢查。
代價是,只要你的資料結構天生要被多處共享又要能改(觀察者模式、圖、快取),
你就得引入 Rc、RefCell、Cell,或乾脆重構所有權。
引用計數走另一個極端。Rc<RefCell<Spaceship>> 讓多個持有者共享同一份
可變資料,別名想怎麼建就怎麼建。它付出的代價是把「同時只能有一個可變借用」這條規則
從編譯期挪到執行期:RefCell 內部維護一個 borrow 計數,你每次
borrow_mut() 都在跟這個計數對賭。編譯器不再幫你把關,你自己得記得
「此刻沒有別人正借著」。
Ante 的別名規則長在一個叫 shape-stability 的不變式上。文章給的定義是: 「a reference to something of stable shape is always valid no matter what mutations are made elsewhere」——只要一個東西的「形狀」穩定,指向它的引用就永遠有效, 無論別處做了什麼修改。這句話聽起來抽象,但落到 struct 上就很具體: 「If you have a mut borrow reference to a struct, you can make a mut borrow reference to one of its fields.」握著一個 struct 的可變借用,你就能再借出它的某個欄位—— 即使這個 struct 是共享可變的,「you can always make a mut borrow reference to a shared mut type's fields, even though they are shared」。
為什麼 struct 欄位可以這樣借?因為 struct 的形狀在編譯期就固定了:每個欄位在物件裡的 偏移量是常數,改欄位的值不會改變欄位的位置,也就不會讓指向另一個欄位的引用失效。 這正是 shape-stability 穩定的東西——不是資料的值,而是資料的佈局。下面這張圖把 struct 與 union 的差別畫出來:struct 的欄位偏移是釘死的,union 的形狀卻會隨 variant 切換而改變,所以借出 union 的 variant 引用就不安全。
對 union 的限制,文章寫得同樣直白:「If you have a mut borrow reference to a union,
you cannot make a mut borrow reference to one of its variants.」握著 union 的可變借用,
你不能再借出它的某個 variant。因為 union 的形狀不穩定——今天它是 Docked,
你借了裡面的 port,別處一個 set 把它切成 InFlight,
你手上那個 port 引用就指向了不再存在的東西。這是 shape-stability 這條
不變式的邊界:它保護 struct,放行 struct 欄位借用,但把 union variant 借用擋在門外。
執行期成本:RefCell 的檢查與 Ante 的零成本
三個模型的執行期開銷差異,全都源自「別名檢查發生在哪裡」。純借用檢查把檢查完全放在 編譯期,執行期一分錢不花——這是 Rust「zero-cost abstraction」口號的實際內容之一。 你享受這個零成本的前提,是接受「共享可變寫不出來」這個約束。
Rc<RefCell<T>> 的執行期成本是兩層疊加。Rc 這層是引用計數:
每次 clone 加一、每次 drop 減一,計數歸零才釋放。RefCell 這層是 borrow
計數:每次 borrow() 或 borrow_mut() 都要讀寫一個內部旗標,
確認此刻的借用不衝突。文章的觀察是,一旦要用 Rust 的引用計數,往往就得搭上 RefCell——
「when we try to use Rust's reference-counting Rc type, it often requires RefCell
(like Rc<RefCell<Spaceship>>)」——這層 borrow 檢查就是 RefCell 帶來的執行期負擔。
Ante 想拿掉的正是 RefCell 這一層。它保留引用計數(共享可變的物件仍靠 RC 管理生命週期), 但把別名檢查放回編譯期。它的目標被明確寫成「blending reference counting and borrow checking for mutable objects, without run-time panics or overhead that come from Rust's RefCell or Swift's exclusivity checking.」——揉合引用計數與借用檢查, 但不要 RefCell 或 Swift exclusivity 檢查帶來的執行期 panic 與開銷。換句話說, Ante 的物件仍有 RC 計數的成本,但沒有 RefCell 的 borrow 計數成本。
做到這件事的關鍵機制,就是把共享可變引用「暫時」轉成獨佔引用。文章把這個 insight
講得很清楚:「we can temporarily get a uniq reference to something, as long as in
that scope we don't access anything else that might reference it」——只要在那個範圍內
不去碰任何其他可能引用到它的東西,我們就能暫時取得一個 uniq 引用。這裡的精妙之處在於
它區分了「存在」與「可用」:「A uniq Spaceship isn't the only reference to the
Spaceship, it's just the only usable reference.」一個 uniq 的 Spaceship
不是這艘船唯一的引用,只是唯一「可用」的引用。
這個「暫時 uniq」轉換是整套設計的樞紐。RefCell 用執行期旗標來保證
「此刻只有我在借」;Ante 用編譯期的作用域分析達到同樣效果——它證明在這個 scope 裡,
別的引用不會被碰到,所以就算物件是共享的,此刻也等同於獨佔。證明成立,就不需要任何
執行期檢查;一份原本共享可變的資料,在證明有效的範圍內被當成獨佔可變來改,
改完離開範圍再回到共享。這是它宣稱「零 RefCell 成本」的具體來源。
把它跟 Rust 的排他性借用擺在一起看,差別在於檢查的時間軸。Rust 要求你在型別上就承諾
整段生命週期內只有一個 &mut,這個承諾一旦立下,編譯器可以放心做基於
「無別名」的最佳化(例如把值留在暫存器裡,不必每次都回讀記憶體)。Ante 的 uniq 是
「借來的」而非「擁有的」——它不改變物件的共享本質,只在一段可證明安全的範圍內臨時
賦予獨佔語意,所以它能套用在那些 Rust 根本不讓你建立的別名結構上。這正是它想同時
站住「共享可變」與「編譯期安全」兩格的技術根據。
崩潰風險:borrow_mut() 的 panic 與 Ante 的編譯期證明
對正在選型的工程師來說,崩潰風險往往比零點幾納秒的檢查成本更要命。這一軸上
Rc<RefCell<T>> 是唯一有問題的一方。文章的措辭是它「can crash at
run-time if you hold it wrong」——握錯了就在執行期崩。所謂握錯,就是在已經有一個
borrow_mut() 活著的時候,又對同一個 RefCell 借了一次。
編譯器對此無能為力,因為別名檢查被推遲到了執行期;違規在跑到那行之前完全隱形,
測試沒覆蓋到的路徑就成了埋在 production 的地雷。
純借用檢查在這一軸上是滿分:別名衝突是編譯錯誤,不是執行期崩潰。你可能覺得借用檢查 「很煩」,但它煩你的時機是編譯期,代價是你的時間,不是使用者的當機。這是 Rust 社群願意忍受借用檢查學習曲線的核心理由——把一整類 bug 移到編譯期解決。
Ante 站在借用檢查這一側,但保留了共享可變的能力。因為 uniq 轉換靠的是編譯期證明,
別名衝突(例如情境①那種兩處同時要可變)會讓證明失敗,於是編譯不過,而不是執行期
panic。它的整條設計主線就是「without run-time panics」——把 RefCell
在執行期做的事,改在編譯期做完。同樣一段共享可變的邏輯,在 RefCell 下你
要在腦中維護「誰現在借著」的時序,賭它不衝突;在 Ante 下這個時序由編譯器檢查,
賭輸的後果從當機變成編譯錯誤。
這不代表 Ante 沒有代價,只是把代價換了個位置——從執行期的不確定性換成編譯期的約束。
情境①和情境④的「編譯不過」就是這個約束的體現:Ante 不允許你寫出它證明不了安全的
共享可變模式。對一個把 Rc<RefCell> 當萬用膠水的 Rust 工程師來說,
這會是需要重新適應的地方——有些在 RefCell 下靠運氣不崩的寫法,在 Ante 下根本編不過。
人體工學與代價:shape-stability 的脆弱面
前三軸 Ante 看起來近乎全勝:共享可變照樣有、執行期不多花 RefCell 的錢、又不冒 panic 風險。但每個抽象都有它付帳的地方,Ante 的帳單記在人體工學這一軸——具體說,記在 shape-stability 這套靜態分析的脆弱性上。
文章對此的原話帶著明顯的保留:「This could be pretty brittle; adding fields to a struct can be a breaking API change.」這套分析可能相當脆弱,替一個 struct 加欄位, 可能就成了破壞性的 API 改動。這句話值得停下來想:在多數語言裡,給 struct 加一個欄位 是最無害的演化操作之一;但當編譯器的 uniq 轉換證明依賴於 struct 精確的形狀與別名關係時, 改動形狀就可能讓原本成立的證明失效,連帶讓下游依賴它的程式碼編不過。這是把安全性 建立在靜態分析上必然要面對的張力——分析越精細,越容易被結構變動打斷。
也正因如此,文章說 Jake 還在找更好的辦法:「for that reason, Jake's looking for a
better way」,而 Ante 整體的方向是「Jake's goal with Ante is to make something
usable, readable, and simple.」讓語言好用、好讀、簡單。這是一個明確的訊號:
temporary-uniq 這套機制是研究中的設計,不是已經定稿、可以拿去寫 production 的成品。
把它跟 Rust 借用檢查(十年打磨、生態成熟)或 Rc<RefCell>(標準庫、
行為完全確定)放在同一張選型表上時,這個成熟度差距是最該記住的一格。
人體工學上還有一個對照面向。Rc<RefCell> 的寫法雖然要到處
.borrow_mut()、要在腦中維護借用時序,但它的心智模型是完全確定的、可以
查文件的;Rust 借用檢查雖然學習曲線陡,但錯誤訊息成熟、社群知識厚。Ante 的
shape-stability 加 uniq 轉換在「寫得順」這件事上很吸引人——共享物件的欄位直接借就好,
不用 borrow_mut()、不用重構所有權——但你換來的是一套還在演化、加個欄位
都可能翻船的分析。這是研究語言與生產語言之間典型的取捨。
值得對照一下 Rust 與 Ante 在「改共享物件欄位」這個最常見需求上的實際寫法。左邊是
Rc<RefCell<Spaceship>> 加 .borrow_mut(),它能編過,
但那行 borrow 是執行期的賭注;右邊是 Ante 直接借共享物件的欄位,靠編譯期的 uniq 轉換
保證安全。拖動中間的分隔線比較兩側。
borrow_mut() 把別名檢查押在執行期,兩處同時借同一 RefCell 就 panic。右:Ante 版本直接借共享物件欄位,別名安全由編譯期 uniq 轉換證明,衝突變成編譯錯誤。右側程式碼為根據文章機制描述整理的示意,非文章逐字引用。左:Rust 版本能編過,但 borrow_mut() 把別名檢查押在執行期,兩處同時借同一 RefCell 就 pa…
改共享物件的欄位,Rust 的 RefCell borrow_mut 是執行期賭注,握錯就 panic;Ante 用編譯期 uniq 轉換借欄位,安全又零成本。
該怎麼選
這三個不是同一個成熟度層級的東西,選型時得先分清楚問的是「今天寫 production 用什麼」
還是「哪個設計方向更對」。純借用檢查與 Rc<RefCell<T>> 都是 Rust
今天就能用的成品;Ante 是研究語言,temporary-uniq 是還在演化、作者自己都說可能脆弱的
設計。所以務實的規則是:production 的共享可變需求,仍然在 Rust 的兩個工具之間選。
如果你的資料天生是樹狀、單一所有權能表達,或共享是唯讀的,純借用檢查是首選——零成本、
編譯期擋掉別名 bug,沒有理由付 RC 或 RefCell 的錢。只有當你真的需要多處共享同一份
可變狀態(圖、觀察者、需要回指父節點的結構),且無法用索引或所有權重構繞開時,
才引入 Rc<RefCell<T>>——並清楚你買下的是執行期 panic 的風險,
得靠自律或 try_borrow_mut() 來守。一個實務上的分界線是:如果別名衝突的
可能路徑多到你沒把握測試全覆蓋,RefCell 的 panic 就是懸在頭上的劍,這時要嘛用
try_borrow_mut() 把崩潰降級成可處理的錯誤,要嘛回頭想辦法讓所有權變單純。
Ante 屬於「值得追蹤、還不能下注」的一格。它想解的問題是真的:共享可變是大量真實程式 的形狀,而 Rust 逼你在「零成本但寫不出來」與「寫得出來但可能崩」之間二選一。Ante 的 shape-stability 加 uniq 轉換給出了一個第三選項的雛形——共享可變、編譯期安全、無 RefCell 成本。如果你在設計自己的語言或分配器、或在評估未來幾年的記憶體模型方向,這篇文章描述的 機制值得讀原文的逐字定義;如果你只是下週要交一個 Rust 服務,這還不是你的工具。
The call:今天寫 production,共享唯讀或樹狀就純借用檢查、非共享不可才上 Rc<RefCell> 並認清 panic 風險;Ante 的共享可變加編譯期 uniq 轉換是最優雅的第三條路,但在作者都說「可能脆弱、還在找更好辦法」之前,它是拿來讀、拿來追蹤的方向,不是拿來下注的工具。