Community程式設計與開發github.com

george-andrikopoulos/domain-rust

一套用於 Rust 開發的工程紀律。涵蓋:先設計型別;在簽名中使用借用;模組可見性需審慎考量;在重要回傳值上使用 #[must_use];廣泛使用 newtype;先廣義解析再進行範圍檢查;封閉的 trait 集合;用於必要欄位的 Typestate Builder;用於協定的 Typestate;驗證抽象化是否被編譯消除;全面非同步;錯誤應命名失敗原因;為每個 clone() 提供正當理由;不要在本機變數上使用 let _ =;解析而非驗證;僅使用私有欄位;跨平台測試固定……

domain-rust 是什麼?

這是一套全面的 Rust 開發工程紀律指南,適用於 Claude Code、Cursor 等 AI 代理和開發者。它提供了超過 20 條經過實戰檢驗的規則,涵蓋從型別設計、所有權管理、錯誤處理到非同步程式設計的各個方面。核心原則是透過型別系統而非文件或測試來保障正確性,例如「先設計型別」要求開發者在撰寫邏輯前先構思型別,「解析而非驗證」則鼓勵在邊界處將原始輸入轉換為豐富的網域型別。其他重點包括在簽名中優先使用借用而非所有權、為每個 clone() 註明理由、使用精心設計的 thiserror 錯誤列舉而非字串、禁止在庫中回傳類型使用 anyhow,以及全面採用非同步以避免阻塞執行緒。許多規則還附帶了可強制執行的鉤子(hooks),例如禁止在生產程式碼中使用 unwrap()、禁止使用 let _ = 忽略本機變數等。這套紀律最適合需要高度可靠性和長期維護性的 Rust 專案,尤其能幫助 AI 代理在生成或審查程式碼時,遵循與經驗豐富的 Rust 工程師相同的嚴謹標準。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/george-andrikopoulos/relearn/tree/HEAD/claude-pack/skills/domain-rust

Installed? Explore more 程式設計與開發 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

domain: rust 規則

非同步必須全面一致 [R:async-all-the-way]

部分由鉤子 no-block-on-in-async 強制執行;其他在非同步程式碼中阻塞執行執行緒的方式——如同步檔案與網路 I/O、跨 await 持有的 std::sync 鎖、未交給 spawn_blocking 的長時間 CPU 區段——以及 src/ 樹之外的任何 Rust 檔案,皆由此指令單獨約束。

非同步程式碼中禁止使用 block_on。非同步執行緒將許多任務複用至少數執行緒上,因此阻塞呼叫不僅延遲一個任務——它會從執行緒池中移除一個工作執行緒,並延遲所有原本應在那裡執行的任務。受影響的任務與導致阻塞的程式碼無關,這就是為何症狀會表現為完全不相關的尾端延遲,並且事後幾乎無法歸因。

block_on 只在同步程式碼進入非同步的邊界處是正確的:例如 main、測試、或來自 C 庫的回呼。一旦進入,就應留在非同步領域中——使用非同步 I/O、在 guard 必須存活於 await 的地方使用非同步感知鎖、並將真正會阻塞的工作(如 CPU 密集計算或同步第三方客戶端)交給 spawn_blocking。

同樣的原則適用於跨 await 持有的 std::sync::Mutex guard。雖然沒有任何標記表示它會阻塞,但它會在任務被取消排程期間持有鎖,導致競爭任務阻塞自己的工作執行緒,而失敗表現為死鎖而非鎖爭用。

在當前執行緒執行緒上巢狀呼叫 block_on 不會導致效能下降——它會直接死鎖。這是誠實的失敗;多執行緒情況只是用執行緒數量掩蓋了相同錯誤,直到負載移除這個隱藏點。

除非函數需要擁有所有權,否則在簽名中使用借用 [R:borrow-in-signatures]

優先使用 &str 而非 String、&[T] 而非 Vec<T>、&Path 而非 PathBuf。簽名是對函數所需內容的聲明,而擁有所有權的參數意味著「我將保留此值」——因此當函數體僅進行讀取時,簽名便不真實,且每個呼叫者都需為此付出分配的代價。

僅在函數儲存該參數的地方擁有所有權。在那種情況下,impl Into<String> 是禮貌的形式:持有 String 的呼叫者無需成本即可移入,而持有 &str 的呼叫者則有意識地進行一次分配。在回傳值方面,優先回傳 &str 而非 &String——額外的間接層對呼叫者毫無益處,並會將欄位的表示形式固定到公開 API 中。

代價很少是單一分配。而是擁有所有權的參數會傳播:呼叫者為了滿足它而 clone,它的呼叫者再為了滿足那個而 clone,一個未經深思選擇的簽名就會變成一連串 clone,每個 clone 在局部看來都是必要的。R:justify-every-clone 會揭露這些 clone;而此規則是關於如何從一開始就避免產生它們。

先透過撰寫型別來設計,再撰寫邏輯 [R:design-types-first]

自撰寫以來已出現 2 次;最近一次為 2026-11-01。

先勾勒型別直到設計浮現,再撰寫邏輯。型別層次的勾勒是測試優先的紅燈階段所追求的「可執行規格」,而且是更強大的規格:測試僅採樣行為空間的點,而型別則約束整個空間,且編譯器能在編譯時、只要有程式碼存在,就證明其正確性。當型別正確時,大部分實作會自行編寫,許多錯誤的實作則無法編譯。在 Rust 中,此順序優先於任何測試優先的預設——測試不會被移除,而是降級到它們作為正確工具的層級:屬性測試用於型別無法編碼的行為法則,單元測試則作為過去錯誤的迴歸固定。將每個保證應用到能維持它的最強層級,並在審查時,首先問的不是「它通過了嗎?」而是「這些保證中,有哪些可以移到更高一層?」

錯誤列舉應攜帶能識別失敗的值 [R:errors-name-what-failed]

部分由鉤子 no-string-error 強制執行;不帶欄位的變體、省略識別值的變體,以及名為 Unknown(String) 的包羅萬象變體(一行程式碼無法將其與攜帶未識別值的變體區分開來),皆由此指令單獨約束。

每個不同的失敗都是 thiserror 列舉的一個變體,且每個變體都攜帶能識別該實例的值:

#[derive(Debug, thiserror::Error)]
pub enum ConnectError {
    #[error("TCP connect to {addr} failed")]
    Tcp { addr: SocketAddr, #[source] source: io::Error },
    #[error("timed out after {elapsed:?}")]
    Timeout { elapsed: Duration },
}

字串無法提供的兩個優點由此產生。呼叫者可以進行 match——重試 Timeout、呈現 Tcp——而不是解析下次有人編輯訊息時就會改變的文字。而且訊息會指名實際的位址,因此一條日誌就足以採取行動,而不是展開調查。

將原因放在 #[source] 中,而不是將其內插到文字中。然後,錯誤鏈會在邊界處一次性列印,無需每一層都重複下一層的內容。

每個呼叫者可能以不同方式處理的條件都應作為一個變體。將四個原因合併為 Other(String) 會在列舉內部重新建立字串錯誤:它看似型別,實則表現如文字,而這正是本規則旨在捕捉的型態。

每個 clone() 都必須攜帶其理由,否則設計有誤 [R:justify-every-clone]

部分由鉤子 no-clone-without-comment 強制執行;註解是否陳述了真實理由而非重複呼叫本身,以及 src/ 樹之外的任何 Rust 檔案,皆由此指令單獨約束。

借用檢查器的錯誤是關於所有權的問題。clone() 並未回答問題;它只是付費來避免回答。應先嘗試回答:重新組織結構以使一個擁有者顯而易見、使用具名生命週期的借用、將借用拆分到較小的欄位、或在使用 Arc/Rc(當值確實是共享而非複製時)分享。

對於那些在審查後仍存活下來的 clone,請在旁邊寫下理由,使用此程式碼庫中通用的格式:

let sources: Vec<RuleTag> = rules.iter().map(|r| r.tag().clone()).collect(); // allow:clone: OutputFile 擁有其來源,其生命週期超出 &Library 借用

這個註解使規則可供審查。未標註的 clone 彼此無法區分,因此審查者必須為每個 clone 重新推導所有權論證,而實際上,對於任何一個都不會重新推導。標註過的 clone 陳述了一個可被檢查的主張,並且在圍繞它的設計發生變化時可以被找到並移除。

理由必須是關於所有權或生命週期。「為了讓它編譯通過」只是錯誤類別的重述,而非正當理由。程式碼認為是同一個值的兩個副本,是一個正確性錯誤,只等著誰修改了錯誤的那個。

存活過審查的大多數 clone 是由簽名而非呼叫點產生的:R:borrow-in-signatures 是壓力的來源,而修正參數通常會刪除 clone,而不是為其加上註解。

模組是封裝邊界;pub 是審慎的匯出 [R:module-visibility-is-deliberate]

自由使用 pub(crate) 和 pub(super);將裸 pub 保留給 crate 審慎匯出的內容。可見性不是文書工作。它是關於哪些內容仍可自由更改的聲明,並且是編譯器唯一能檢查的此類聲明。

預設值很重要,因為 pub 易於添加卻難於移除。一旦某個項目變為公開,外部呼叫者可能依賴於它,因此模組無法再重組、欄位無法重新命名、為單一呼叫點編寫的輔助函數現在成為了支援的 API。這些決定從未被做出過;它們只是被預設值所決定。

模組如同型別一樣,維持著一個不變量。一個只能在檢查之後才能呼叫的輔助函數,應與該檢查一起放在模組的私有區域中,這樣「只能在之後」就能因為其他地方沒有人能呼叫它而得到強制執行。將其設為公開,則會將這個保證轉化為文件註解。

應在 crate 根目錄使用 pub use 明確地重新匯出預期的表面 API,並讓其背後的所有內容盡可能保持私有。公開 API 於是成為了一份由某人撰寫的清單,而不是程式碼恰好所在位置的殘留物。

R:private-fields-only 是同樣的論點,只是低了一個層級:作用於欄位而非項目。兩者是可以分開的——公開型別上的私有欄位會洩漏結構,而 pub(crate) 型別上的公開欄位則不會——因此滿足其中一個並不能說明另一個。

在重要的回傳值上標記 #[must_use] [R:must-use-on-consequential-returns]

在所有回傳 Result 的函數上,以及在所有回傳值為呼叫該函數之目的的函數上,添加 #[must_use]。如此一來,編譯器會在每個呼叫點(現在與未來)標記被丟棄的結果,而不是留給審查者在一百行程式中注意到一個孤立的語句。

優先將屬性放在型別上而非函數上。#[must_use] struct Receipt; 會傳播到每個回傳 Receipt 的函數,包括那些後來由從未讀過此規則的人編寫的函數;而函數層級的屬性則需要在每次使用時記住。當型別始終是重要的時,應將其附加到型別上;僅在相同型別有時可以被丟棄這種較窄的情況下,才退而使用函數層級的屬性。

將產生的警告升級為 CI 中的錯誤。一個違反 #[must_use] 只會印出沒人看的筆記,這不過是帶有額外語法的註解,而其聲稱的保證則無處可尋。

廣泛使用 newtype:不同的概念使用不同的型別 [R:newtype-liberally]

為每個領域概念賦予自己的型別,即使底層表示相同。Miles(f64) 與 Kilometers(f64)、Host(String) 與 Port(u16)——永遠不要使用兩個裸 f64 或一個 String 與一個 u16 且順序只有人類記得。Newtypes 通常是零成本的:它們編譯成與基本型別相同的機器碼,因此它們唯一增加的是你想要的編譯錯誤——但當這種成本是關鍵時,R:verify-the-abstraction-compiled-away 要求檢查這個主張而不是重複它。無法被寫出的單位混淆錯誤比在審查中捕獲的錯誤成本更低,而遠低於那些未被捕獲的錯誤。R:parse-dont-validate 是 newtype 的來源——邊界將其鑄造為見證者;此規則是關於之後將其隨處攜帶,而不是拆解回基本型別。

庫中禁止使用 anyhow 作為回傳型別 [R:no-anyhow-in-libraries]

同時由鉤子 no-anyhow-in-lib 強制執行。

庫 crate 應回傳型別化的錯誤列舉(使用 thiserror),這樣 Result 就能準確說出可能出錯的內容,且呼叫者可以對其進行 match。anyhow 僅屬於最外層的二進位邊界。此規則已成熟:no-anyhow-in-lib 鉤子現在能在撰寫時確定性地強制執行它。

不要使用 let _ = 來忽略未使用的區域變數;使用它、回報它、或刪除它 [R:no-let-underscore-on-a-local]

部分由鉤子 no-let-underscore-local 強制執行;底線前綴的繫結由此指令單獨約束。

編譯器的未使用繫結警告是一個讀者。它指出你花費功夫計算出的值最終毫無用處。let _ = x; 的存在就是為了關閉這個讀者,而對於區域變數,它幾乎總是在不該關閉的時候關閉了它。

它有兩種形式,兩者在 -D warnings 下都能乾淨編譯,但對審查者來說卻讀起來像是某人做出的決定:

  • 一個被計算出又被丟棄的區別。 一個旗標被解析、一個模式被解碼、一個設定值被讀取——然後被丟棄,因此本應被區分的案例被默默地當作一個案例處理。
  • 一個被捕獲又被丟棄的失敗。 if let Err(e) = op() { let _ = e; continue; } 不會留下任何事件;失敗發生了,而系統中沒有任何東西之後能說明此事。

當警告觸發時,請執行以下三件事之一。使用該值——一個未被使用的區別通常表示缺少一個分支,而警告正在指向它。回報或傳遞它——一個真正安全到可以忽略的錯誤仍應獲得一個命名事件,或一個註解說明為什麼下游沒有人需要知道。或者刪除計算出它的程式碼。 當一個繫結在設計上就是死的(例如迴圈變數的最後一次賦值),應改寫迴圈以避免產生這個死賦值,而不是讓警告保持靜默。

在呼叫上使用 let _ = f(); 是一個不同的情況:它丟棄了回傳值,這由 R:must-use-on-consequential-returns 管轄。此規則關乎於你已經持有的區域變數。R:guarantee-needs-a-reader 依賴編譯器來捕捉未讀取的繫結;let _ = 是它無法看到的一種寫法。

失敗模式檢查:每當你即將寫下 let _ = name; 時,問問自己:這個值被計算出來是為了決定什麼,而那個決定現在發生在哪裡? 如果答案是「無處可尋」,那麼警告就是對的。

生產程式碼中禁止使用 unwrap() [R:no-unwrap-in-production]

同時由鉤子 no-unwrap-in-src 與 no-expect-empty-msg 強制執行。

生產程式碼中禁止使用 unwrap()。僅在使用有意義的 panic 訊息(能指明資源與不變量)時使用 expect(),或者使用 ? 回傳錯誤,讓呼叫者決定如何呈現它。此規則已成熟:no-unwrap-in-src 鉤子現在能在撰寫時確定性地強制執行它,因此指令層級已無需再處理。

解析,而非驗證 [R:parse-dont-validate]

部分由鉤子 no-free-validator 強制執行;在內部呼叫點重新檢查已解析的資料由此指令單獨約束。

在最外層邊界將原始輸入轉換為豐富的領域型別,產生一個見證 newtype,其存在證明了檢查已發生。內部程式碼使用見證且從不重新檢查——一個邊界、一次檢查、之後由編譯器在所有地方強制執行。R:parse-wide-then-range-check 進一步強化了這一點:邊界必須能夠看到非法值,以便為其命名。

先廣義解析,再進行範圍檢查 [R:parse-wide-then-range-check]

解析為一個足夠寬廣以能表示超出範圍值的型別,然後再進行範圍檢查……

[已截斷以利翻譯]

Individual skills in this repo

This repo contains 3 individual skills — each has its own dedicated page.

george-andrikopoulos/domain-java

Engineering discipline for working in java. Covers: a speculated path deoptimises when the input changes; a view is not a copy; a wrapper type is not free here; close what you open; design for inheritance or forbid it; equality is one contract; exceptions name what failed; identity is not equality for boxes; no reference to internals escapes; null is not a value; publish safely or not at all; seal the alternatives; serializable is a second constructor

george-andrikopoulos/domain-low-latency

Engineering discipline for working in low-latency. Covers: attack the design in a second pass; a measurement matches the regime it reports; a queue without a bound has no overload behaviour; a structure keeps the regime it was proved under; allocated is not resident; answer the requirement at its layer; no allocation on the hot path; no coordinated omission; no false sharing on a hot line; no retry loop on a contended path; no stall inside a publication window; no syscall on a bounded path;...

george-andrikopoulos/global

Engineering discipline that applies to every project and language. Covers: guarantee needs a reader; verdict survives the channel; check the claim you inherit; doc currency; measure the claim not a subset; no silent spend; prefer by construction; repair the lying artefact; signal needs a consequence; verify through production path; detector excludes own definitions; an ambiguous write target is a question; anchor outside the checked partys reach; delegate a fan out; diff against the known...

相關技能