在属性包装器篇的结尾,我埋下了“带 @ 的不一定都是包装器”这一伏笔。主角就是 @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)