在屬性包裝器篇的結尾,我埋下了「不是所有加上 @ 的東西都是包裝器」這個伏筆。主角就是 @Observable。
本文接續上一篇文章 Swift 深入解析 #6。
外觀看起來像包裝器,但本質上是巨集。無論是在 Xcode 使用 #Preview,還是在 SwiftData 使用 @Model,我們早已是巨集的使用者。
本篇深入解析 Swift 5.9 引入的巨集是什麼、能解決哪些問題,以及需要付出什麼成本。SE-0382: Expression Macros
巨集解決的問題 — 樣板程式碼的最後堡壘
Swift 持續加入減少重複程式碼的機制:協定預設實作、Codable 自動合成,以及屬性包裝器。
然而,仍有一個工具無法處理的領域:讀取型別的結構,並產生符合該結構的程式碼。
Observation 就是很好的例子。若要在屬性變更時通知觀察者,每個儲存屬性都需要追蹤程式碼。
使用 @Published 這類包裝器時,每個屬性都必須加上 @;漏加的屬性就會悄悄地不在觀察範圍內。
我們需要的是:「請為這個類別的所有儲存屬性,依各自名稱產生對應的程式碼。」這必須讀取型別結構,超出了包裝器的能力。
傳統上,這個領域由兩種方式負責:具有執行階段成本與不透明性的 Objective-C 執行階段動態技術,或是在建置流程之外另行管理的 Sourcery 等外部程式碼產生工具。
巨集把這項工作帶進語言本身,而且是在編譯時執行。
運作原理 — 嵌入編譯器的程式碼產生外掛
Swift 巨集的本質是編譯器外掛,與 C 前置處理器的文字取代有根本上的不同。運作順序如下。
- 編譯器在原始碼中遇到巨集使用方式(# 或 @)時,會將該程式碼的語法樹(AST)傳給巨集實作。
- 巨集實作是以獨立程序執行的 Swift 程式。它使用 SwiftSyntax 函式庫分析樹狀結構,建立新的程式碼片段後回傳。
- 編譯器會將結果接合回原本的位置,接著便照常編譯。
這種結構帶來幾項重要特性。
第一,產物是真正可檢查的 Swift 程式碼。在 Xcode 中對巨集按右鍵並選擇 Expand Macro,就能直接展開產生的程式碼。
隨時都能掀開魔法帷幕,這就是 Progressive Disclosure 篇提到的「隱藏,但不遮蔽」的巨集版本。
第二,巨集具備衛生性(hygienic)。巨集建立的變數會受到管理,以避免與周圍程式碼的名稱衝突;巨集也無法觸碰其宣告角色範圍之外的程式碼。
C 巨集惡名昭彰的副作用,在設計階段就被阻擋了。
第三,巨集在沙盒中執行。巨集程序無法存取檔案或網路,因此不會成為在編譯時執行任意程式碼的安全漏洞。
兩大陣營 — freestanding(#) 與 attached(@)
巨集依附加方式分成兩大類。
freestanding 巨集(#)會在所在位置產生程式碼。 #Preview { MyView() }則會建立預覽註冊結構。
#URL("https://apple.com")類型的巨集會在編譯時驗證字串,並產生已解除包裝的 URL。這類巨集會出現在應放置運算式的位置。
**attached 巨集(@)會擴充所附加的宣告。**其角色還能進一步細分。
包括新增成員的 member、為屬性加入存取子的 accessor,以及新增協定採用的 extension。同一個巨集也可以兼具多種角色。
@Observable 正是這種組合。它為類別的每個儲存屬性加入觀察追蹤存取子(accessor),新增註冊儲存區成員(member),並加入對 Observable 協定的採用(extension)。
這就是 @Published 時代必須為每個屬性加上 @ 的不便,能縮減成只需在型別上加一個 @ 的原因。
了解這項區別後,函式庫文件就容易讀懂了。想想 SwiftData 的 @Model、測試框架的 #expect(Swift Testing 篇介紹過的那個巨集),以及 Composable Architecture 的 @Reducer。
近期生態系中重要的 API,全部都屬於這兩大陣營之一。
成本帳單 — 要自己打造,還是只負責使用?
巨集有明確的成本,而使用者與製作者所承擔的計算方式不同。
**使用者端的成本主要是建置時間。**巨集實作依賴 SwiftSyntax,而它是很大的函式庫。
因此,第一次建置使用巨集的套件時,會完整加入 SwiftSyntax 的編譯。
CI 的乾淨建置常會增加數分鐘,社群也持續改進發佈預先建置二進位檔等緩解方案。
即使如此,對使用者而言,多數情況下仍是可以接受的成本。因為可以用 Expand Macro 檢查,而且沒有執行階段成本。
**製作者端的成本高得多。**撰寫巨集就是撰寫「處理程式碼的程式碼」,難度本身高了一個層級。
還需要學習 SwiftSyntax API、撰寫比較輸入程式碼與預期輸出程式碼的巨集專用測試,以及設計診斷訊息。
因此,實務上採取保守標準較為妥當。能以協定預設實作、泛型或包裝器解決的問題,應先採用那些方式。
只有當團隊程式碼庫中廣泛存在「必須讀取型別結構的重複」時,自製巨集才值得列入候選。這就是 YAGNI(You Aren’t Gonna Need It — 在需要之前不要建立)的巨集版本。
對大多數團隊而言,巨集不是用來打造的工具,而是用來準確理解並使用框架所提供功能的工具。
總結
- 巨集是編譯器外掛,會在編譯時接收語法樹並產生程式碼(Swift 5.9)。不同於進行文字取代的 C 巨集,它能讀取型別結構,並在具衛生性的沙盒中執行。
- freestanding(#)會在所在位置產生程式碼(#Preview),attached(@)則會擴充所附加的宣告(@Observable、@Model)。
- 產生結果是真正的 Swift 程式碼,隨時都能用 Expand Macro 檢查。不是所有加上 @ 的東西都是包裝器,因此現在可以在閱讀文件時辨識 macro 這個詞。
- 成本是使用者端由 SwiftSyntax 帶來的建置時間,以及製作者端較高的實作難度。只有確認存在「必須讀取型別結構的廣泛重複」時,才適合自製。SE-0389: Attached Macros
下一篇將介紹 Swift 從 Rust 領域帶來的概念:禁止複製的型別 ~Copyable,以及 borrowing 與 consuming 開啟的所有權世界。
來源與確認標準
- SE-0389: Attached Macros — Swift Evolution · 標準與規格原文 · 確認 2026-08-17 · 依據:attached macro 的角色與可產生的宣告
- SE-0382: Expression Macros — Swift Evolution · 標準與規格原文 · 確認 2026-08-17 · 依據:Swift 5.9 expression macro 模型與編譯器外掛

![[Swift 深入解析 #7] Swift 巨集總整理,@Observable 的真面目 封面圖](/assets/images/posts/2c0c42f3-4f02-48e1-8e36-2d9f4f1bf9b6/swift-macros-compiler-plugin.jpg)