Swift 與 Objective-C

[Swift 深入解析 #7] Swift 巨集總整理,@Observable 的真面目

Swift 5.9 的巨集是嵌入編譯器的程式碼產生外掛。本篇整理接收語法樹並回傳程式碼的原理、freestanding(#) 與 attached(@) 兩大類,以及自行建立時必須付出的成本。

閱讀 7 分鐘
[Swift 深入解析 #7] Swift 巨集總整理,@Observable 的真面目 封面圖

在屬性包裝器篇的結尾,我埋下了「不是所有加上 @ 的東西都是包裝器」這個伏筆。主角就是 @Observable。

本文接續上一篇文章 Swift 深入解析 #6。

外觀看起來像包裝器,但本質上是巨集。無論是在 Xcode 使用 #Preview,還是在 SwiftData 使用 @Model,我們早已是巨集的使用者。

本篇深入解析 Swift 5.9 引入的巨集是什麼、能解決哪些問題,以及需要付出什麼成本。SE-0382: Expression Macros

巨集解決的問題 — 樣板程式碼的最後堡壘

Swift 持續加入減少重複程式碼的機制:協定預設實作、Codable 自動合成,以及屬性包裝器。

然而,仍有一個工具無法處理的領域:讀取型別的結構,並產生符合該結構的程式碼。

Observation 就是很好的例子。若要在屬性變更時通知觀察者,每個儲存屬性都需要追蹤程式碼。

使用 @Published 這類包裝器時,每個屬性都必須加上 @;漏加的屬性就會悄悄地不在觀察範圍內。

我們需要的是:「請為這個類別的所有儲存屬性,依各自名稱產生對應的程式碼。」這必須讀取型別結構,超出了包裝器的能力。

傳統上,這個領域由兩種方式負責:具有執行階段成本與不透明性的 Objective-C 執行階段動態技術,或是在建置流程之外另行管理的 Sourcery 等外部程式碼產生工具。

巨集把這項工作帶進語言本身,而且是在編譯時執行。

運作原理 — 嵌入編譯器的程式碼產生外掛

Swift 巨集的本質是編譯器外掛,與 C 前置處理器的文字取代有根本上的不同。運作順序如下。

  1. 編譯器在原始碼中遇到巨集使用方式(# 或 @)時,會將該程式碼的語法樹(AST)傳給巨集實作。
  2. 巨集實作是以獨立程序執行的 Swift 程式。它使用 SwiftSyntax 函式庫分析樹狀結構,建立新的程式碼片段後回傳。
  3. 編譯器會將結果接合回原本的位置,接著便照常編譯。

這種結構帶來幾項重要特性。

第一,產物是真正可檢查的 Swift 程式碼。在 Xcode 中對巨集按右鍵並選擇 Expand Macro,就能直接展開產生的程式碼。

隨時都能掀開魔法帷幕,這就是 Progressive Disclosure 篇提到的「隱藏,但不遮蔽」的巨集版本。

第二,巨集具備衛生性(hygienic)。巨集建立的變數會受到管理,以避免與周圍程式碼的名稱衝突;巨集也無法觸碰其宣告角色範圍之外的程式碼。

C 巨集惡名昭彰的副作用,在設計階段就被阻擋了。

第三,巨集在沙盒中執行。巨集程序無法存取檔案或網路,因此不會成為在編譯時執行任意程式碼的安全漏洞。

將語法樹傳給沙盒外掛並取回產生程式碼的四步驟示意圖
接收語法樹並回傳程式碼,結果可透過 Expand Macro 檢查

兩大陣營 — 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 模型與編譯器外掛

延伸閱讀