vatt'ghern jaskier's ballads
本文 1 個互動圖表在手機上以重點摘要呈現,互動版請以桌面瀏覽器開啟。

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 個判準

純借用檢查、純引用計數、Ante 混合在七個判準上的落點(資料來源:verdagon.dev 2026-06-28)
判準 純借用檢查(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 的靜態分析可能因為改結構而失效。

表只是把答案先攤開。真正要理解的是為什麼 Ante 能同時佔住兩個看似矛盾的格子。 它的核心工具有兩個:一個叫 shape-stability 的靜態不變式,決定了哪些引用「永遠有效」; 以及一個把共享可變引用「暫時」轉成獨佔引用的規則。下面這個互動圖把三種方案放進同一組 別名情境裡——點選一個情境,看三方各自的判決。

點任一情境看三方判決 · 4 種別名情境 × 3 種方案

別名情境(點選) ① 兩處同時要可變存取同一物件 two &mut aliases, both live ② 借出共享物件的一個 struct 欄位 mut borrow one field of a shared struct ③ 暫時獨佔存取,範圍內不碰其他別名 temporary uniq, no other reference touched in scope ④ 借出共享 union / enum 的一個 variant mut borrow one variant of a shared union

情境① · 兩處同時要可變

純借用檢查編譯不過。「we can't have multiple &mut references pointing to the same data.」
Rc + RefCell兩處都 borrow_mut → 執行期 panic(同一 RefCell 借兩次)
Ante編譯不過,因為範圍內同時碰到兩個別名,uniq 轉換無法成立

情境② · 借共享物件的欄位

純借用檢查共享的前提下寫不出來,得先重構所有權
Rc + RefCell可以,但要先 borrow_mut 整個物件
Ante直接借。「you can always make a mut borrow reference to a shared mut type's fields」

情境③ · 暫時獨佔

純借用檢查共享物件上談不上「暫時獨佔」
Rc + RefCellborrow_mut 拿到獨佔,但保證靠執行期 flag,握錯仍會 panic
Ante安全。編譯期證明範圍內不碰別名 → 暫時轉成 uniq,零成本

情境④ · 借共享 union 的 variant

純借用檢查單一所有權下可以 match 後借
Rc + RefCellborrow_mut 後 match,一樣冒 panic 風險
Ante不允許。「you cannot make a mut borrow reference to one of its variants」
同一組情境,三方判決各異。Ante 只在①和④說「不」,其餘兩格拿到了借用檢查拿不到、又不必付 RefCell 代價的結果。①④的「不」正是它靜態安全的來源。

別名規則:誰能同時握住兩個可變引用

別名規則是這三個模型的分水嶺。Rust 的立場最硬:一份資料在任一時刻,最多只能有一個 &mut。文章直接引用這條規則——「In Rust, we can't have multiple &mut references pointing to the same data.」——它是 Rust 記憶體安全的基石, 也是借用檢查零成本的原因:既然編譯器已經證明沒有共享可變,執行期就不需要任何檢查。 代價是,只要你的資料結構天生要被多處共享又要能改(觀察者模式、圖、快取), 你就得引入 RcRefCellCell,或乾脆重構所有權。

引用計數走另一個極端。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 引用就不安全。

STRUCT · SHAPE-STABLE UNION · SHAPE-CHANGING hp: Int @ offset 0 fuel: Int @ offset 8 name: Str @ offset 16 偏移量固定 → 借 fuel 欄位永遠有效 改 hp 不會移動 fuel,指向 fuel 的引用不失效 variant Docked port: Int ↓ 切換 variant variant InFlight { vel: Vec3 } 形狀隨 variant 改變 → 借 variant 不安全 切成 InFlight,指向 Docked.port 的引用就懸空
shape-stability 穩定的是佈局,不是值。struct 的欄位偏移在編譯期釘死,改任一欄位不會移動其他欄位;union 一旦切 variant,內部佈局整個換掉,任何指向舊 variant 內部的引用都可能懸空。這就是為什麼 Ante 允許借共享 struct 的欄位、卻不允許借共享 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 不是這艘船唯一的引用,只是唯一「可用」的引用。

Spaceship RC = 3 此 scope 內:只有 ref_a 被存取 ref_a usable · uniq ref_b exists · not touched ref_c exists · not touched 編譯器證明 scope 內只碰 ref_a → ref_a 暫時是唯一「可用」引用 → 可當 uniq 安全改欄位
三個引用都指向同一艘船(RC = 3),但這個 scope 裡只有 ref_a 被存取。編譯器據此證明:在這段程式碼執行期間,別人不可能透過 ref_b、ref_c 觀察到船,於是把 ref_a 暫時當成 uniq。它不是唯一存在的引用,只是唯一可用的引用——這就是零成本、無 panic 的可變存取。

這個「暫時 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 轉換 保證安全。拖動中間的分隔線比較兩側。

// Rust: Rc<RefCell<T>>,借用檢查在執行期 let ship = Rc::new(RefCell::new(Spaceship::new())); let a = Rc::clone(&ship); let b = Rc::clone(&ship); // 多個共享持有者 // 改一個欄位:必須先 borrow_mut a.borrow_mut().fuel += 10; // 執行期借用計數 // 若此時 b 還借著,這行 panic: // already mutably borrowed: BorrowMutError b.borrow_mut().hp -= 5; // 握錯就在 runtime 崩
// Ante: 共享可變 + 編譯期 uniq 轉換 ship = new Spaceship () a = ship b = ship // 一樣是共享可變 // 直接借共享物件的欄位,無 RefCell a.&fuel += 10 // shape-stable,編譯期證明 // scope 內不碰 b,編譯器把 a 當 uniq // 別名衝突 = 編譯不過,不是 runtime panic a.&hp -= 5 // 零 RefCell 成本、零 panic 風險
Rc<RefCell> Ante
左:Rust 版本能編過,但 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 轉換是最優雅的第三條路,但在作者都說「可能脆弱、還在找更好辦法」之前,它是拿來讀、拿來追蹤的方向,不是拿來下注的工具。