1. 宏的定义
Swift 宏(Macro) 是一种在编译期执行的代码生成机制。开发者可以通过简洁的语法标记,在源代码中自动插入、替换或补充逻辑,从而实现样板代码的自动化。
Swift 宏建立在语法树与类型系统之上,具备类型安全、语义明确与可预测的元编程特性。
为什么使用宏?
Swift 宏的优势体现在以下几个方面:
- 编译期执行,零运行时开销 宏在编译阶段完成代码展开,避免运行时反射或动态调用的性能负担。
- 减少样板代码,提升开发效率 无需手动实现
Equatable
、Codable
、监听器等重复性逻辑,宏可以自动生成这些代码。 - 类型安全,语法无缝衔接 宏展开后的代码与手写代码一样,会经历完整的语法与类型验证,确保可靠性与一致性。
2. 宏的设计原则
Swift 宏的设计秉承“显式、安全、可预测”三大原则,避免“魔法式”的隐式行为:
原则 | 说明 |
---|---|
显式调用 | 宏必须通过明确语法标记使用,开发者清晰可见。 |
类型检查 | 宏生成的代码会经过完整的类型系统验证,不会绕过语言规则。 |
可预测展开 | 宏的展开逻辑必须是稳定的、可预期的,结果不会因外部环境而改变。 |
宏不是魔法,它并不神秘,也不凌驾于语言规则之上。你写下的每一个宏调用,都将以可读、可测、可调试的方式插入源代码中。
3. 宏的原理
Swift 宏基于编译器插件(Compiler Plug-in) 机制运行,整个过程发生在编译期,并受到严格的沙盒限制。
宏的展开流程
- 提取宏调用:编译器识别源码中的宏语法,并生成对应的原始语法树(Raw Syntax Tree)。
- 发送到宏插件:宏语法树被发送至对应插件,该插件在沙盒中以独立进程运行。
- 执行宏逻辑:插件处理语法树并生成新的代码片段(语法节点)。
- 插入并继续编译:新生成的语法节点被插入原始源码,参与后续的编译过程。
宏的安全性与纯粹性
为了确保宏系统的 安全、稳定与可控性,Swift 从两个维度对宏行为做出约束:
系统隔离:沙盒机制
所有宏插件运行在独立的沙盒进程中,Swift 对其能力进行了严格限制:
- ✖️ 禁止访问文件系统(如
FileManager
) - ✖️ 禁止发起网络请求
- ✖️ 禁止调用系统级 API
这些限制是编译器层面的强制规定,一旦访问受限资源,会立即报错,例如:
"The file “xxx” couldn’t be opened because you don’t have permission to view it."
因此,即使使用第三方宏插件,也无需担心其在背后执行未授权的操作。
设计哲学:纯粹性原则
Swift 鼓励将宏视为纯函数 —— 相同输入始终生成相同输出。这有助于:
- 提高宏行为的可预测性
- 避免构建结果因环境不同而变化
- 支持编译器缓存宏结果,提升性能
推荐做法
- ✔️ 仅依赖编译器传入的语法树与上下文
- ✔️ 避免访问系统环境、网络、文件
- ✔️ 生成稳定、可测、可重现的代码
不建议行为
- ✖️ 使用
UUID()
或Date()
等生成动态值 - ✖️ 使用随机数作为默认值
- ✖️ 在多个宏之间共享全局上下文或隐式状态
这些行为虽然 技术上允许,但会破坏宏的一致性,导致难以调试、不可复现的构建结果。
4. 宏角色与命名说明符:Swift 宏的职责与命名控制
Swift 宏并非千篇一律,它具备明确的职责划分,这种职责由编译器通过一套称为 宏角色(Macro Role) 的机制识别和执行。
4.1 宏角色:Swift 宏的功能标识
宏角色决定了一个宏可以做什么。Swift 中的宏主要分为两类:
- 独立宏(Freestanding) :使用
@freestanding(...)
标记,独立于任何已有声明,适合生成表达式或新的声明语句。 - 绑定宏(Attached) :使用
@attached(...)
标记,附着在已有声明(如类型、函数、属性)上,用于扩展或修改它们的结构。
每种宏角色都对应特定的协议,定义其展开行为:
宏角色 | 描述 | 协议名 | 示例用途 |
---|---|---|---|
@freestanding(expression) | 表达式独立宏 | ExpressionMacro | 替换或扩展表达式 |
@freestanding(declaration) | 声明式独立宏 | DeclarationMacro | 添加变量、函数、类型声明 |
@attached(member) | 成员绑定宏 | MemberMacro | 向类型中注入属性、方法等成员 |
@attached(peer) | 对等绑定宏 | PeerMacro | 在声明旁插入并列的新声明 |
@attached(accessor) | 访问器绑定宏 | AccessorMacro | 自动生成 get/set 等属性访问器 |
@attached(extension) | 扩展绑定宏 | ExtensionMacro | 生成扩展(extension) |
@attached(memberAttribute) | 成员属性绑定宏 | MemberAttributeMacro | 修改成员的注解、属性等 |
@attached(body) | 函数体替换绑定宏 | BodyMacro | 替换计算属性或函数的实现体 |
独立宏以
#宏名(...)
使用,绑定宏以@宏名(...)
使用。
这些角色为 Swift 宏构建起了清晰的职责体系 —— 每个宏角色都对应一类语法结构的生成或修改行为。
4.2 命名说明符:绑定宏中的命名控制器
对于会生成 具名实体(如属性、函数、类型) 的宏,Swift 提供了另一套机制来进一步控制“生成出来的东西叫什么”,这就是 命名说明符(Name Specifier) 。
在绑定宏(例如 MemberMacro
、AccessorMacro
)中,我们通常使用 expanded
方法返回字符串形式的声明代码。但如果不明确命名,编译器将视这些内容为 匿名生成,从而带来几个问题:
- 无法在语义层面识别生成成员的名称;
- 代码补全、跳转、文档工具支持不佳;
- 多个宏同时生成代码时容易发生命名冲突;
- 其他宏无法可靠地引用这些生成内容。
为了解决这些问题,Swift 引入了 命名说明符 机制,用于精确指定宏生成的实体名称。例如:
@attached(extension, names: named(==))
这表示:宏将生成一个具名为 ==
的成员方法。
命名说明符的种类与用途
命名说明符 | 典型用途 | 原始声明 | 宏生成结果 |
---|---|---|---|
named("...") | 设定固定名称 | struct MyView {} | static func makePreview() |
prefixed("...") | 给生成成员加前缀 | var name: String | var debug_name: String |
suffixed("...") | 给生成成员加后缀 | func save() | func saveAsync() |
overloaded | 添加重载版本 | func log() | func log(level: LogLevel) |
arbitrary | 自定义命名(复杂场景) | struct User {} | _UserFlagsHelper , internalMap 等 |
5. 宏协议:决定宏行为的功能接口
Swift 宏的功能是建立在一套明确分层的协议体系上的。这些协议定义了宏的 基本行为、适用场景,以及 如何响应编译器的宏展开请求。
5.1 宏的基础协议:Macro
所有 Swift 宏都遵循 Macro
协议,它是宏体系的根基,定义了宏的基本能力和默认行为。
public protocol Macro {
/// 控制宏展开后的代码是否格式化,默认为 `.auto`
static var formatMode: FormatMode { get }
}
.auto
(默认):使用格式化后的展开代码,推荐使用,能保持代码一致性。.disabled
:展开后的代码将原样插入,不进行格式化,适用于自定义输出。
5.2 宏的分类协议:FreestandingMacro
与 AttachedMacro
在 Macro
协议的基础上,Swift 将宏分为两类:
FreestandingMacro
:用于 独立使用的宏,可以直接插入到表达式、声明等任何地方,适合用来生成简单的表达式。
public protocol FreestandingMacro: Macro { }
AttachedMacro
:用于 附着在已有代码上的宏,必须绑定到已有的类型、属性、函数等声明上,适合对已有代码进行扩展。
public protocol AttachedMacro: Macro { }
这两个协议本身不定义任何具体行为,它们为更细分的角色协议提供了基础。
💡 Swift 使用协议体系来设计宏的目的是:
- 层次清晰:基础协议定义宏的公共行为,高层协议划分宏的使用场景,角色协议定义宏的具体能力。
- 编译器驱动:根据宏的角色和位置,编译器调用特定协议中的
expansion(...)
方法展开宏。- 类型安全:协议方法的定义明确,展开时处理的语法结构与上下文类型都有严格的检查。
5.3 宏的角色协议
每个宏的角色都需要实现一个静态方法 expansion(of:in:)
,这是编译器在宏展开时调用的核心方法。该方法将接收当前语法节点和上下文信息,并返回生成的语法树,最终插入到用户代码中。
💡 一个宏的实现可以遵循多个协议,从而具备多重角色能力。 例如,以下
AutoCodableMacro
同时实现了MemberMacro
和AccessorMacro
,因此它具备生成成员和访问器的能力:
public struct AutoCodableMacro: MemberMacro, AccessorMacro {
public static func expansion(...) -> [DeclSyntax] { ... }
public static func expansion(...) -> [AccessorDeclSyntax] { ... }
}
这正是 Swift 宏系统的强大之处 —— 通过协议组合实现宏的 多重角色。
5.4 主要宏角色协议
接下来,我们将一一解析不同的角色协议,详细说明每个协议的职责、调用时机及适用场景。
1. 表达式独立宏:ExpressionMacro
public protocol ExpressionMacro: FreestandingMacro {
static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> ExprSyntax
}
功能:插入和替换表达式。适用于动态计算、生成常量、包装表达式等。
2. 声明式独立宏:DeclarationMacro
public protocol DeclarationMacro: FreestandingMacro {
static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax]
}
功能:用于插入新的声明(例如,变量、函数、类型声明等)。
3. 对等绑定宏:PeerMacro
public protocol PeerMacro: AttachedMacro {
static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax]
}
功能:在现有声明旁边生成平级结构,通常用于插入同级声明。
4. 访问器绑定宏:AccessorMacro
public protocol AccessorMacro: AttachedMacro {
static func expansion(
of node: AttributeSyntax,
providingAccessorsOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [AccessorDeclSyntax]
}
功能:为属性添加访问器(如 get
、set
、didSet
等)。
5. 成员属性修饰宏:MemberAttributeMacro
public protocol MemberAttributeMacro: AttachedMacro {
static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingAttributesFor member: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [AttributeSyntax]
}
功能:为成员添加统一的修饰符或属性标签。
6. 成员绑定宏:MemberMacro
public protocol MemberMacro: AttachedMacro {
static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax]
static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [DeclSyntax]
}
功能:为类型添加成员(如属性、方法、构造器等)。
7. 替换声明体绑定宏:BodyMacro
public protocol BodyMacro: AttachedMacro {
static func expansion(
of node: AttributeSyntax,
providingBodyFor declaration: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax,
in context: some MacroExpansionContext
) throws -> [CodeBlockItemSyntax]
}
功能:为现有声明提供具体实现或行为,常用于生成计算属性的实现或补充函数体。
8. 扩展绑定宏:ExtensionMacro
public protocol ExtensionMacro: AttachedMacro {
static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax]
}
功能:为类型生成扩展,通常用于协议一致性等。
9. 未公开使用的实验宏
CodeItemMacro
:用于插入宽泛的代码片段。 PreambleMacro
:为文件自动注入文件级代码。
6. 宏的结构设计:从角色到行为的思维路径
Swift 宏系统之所以强大,在于它并不追求“全能型”宏,而是通过“角色划分”将每种宏限制在特定场景中。这不仅让系统具备类型安全与上下文约束,还帮助开发者在设计宏时建立起清晰的思维路径。
本章我们将构建这样一个模型:每一种宏的“角色” → 应该遵循的“协议” → 实现的“行为结构” 。
并通过一些实际例子,帮助你理解 如何基于宏的使用意图,选择正确的协议与输出结构。
6.1 宏角色简析:你要扩展什么?
一个宏所作用的语法位置被称为它的 角色(Role) 。角色决定了宏能应用在哪类语法结构上(如表达式、属性、类型、函数体等),也决定了宏展开时能生成哪类代码结构。
你想做什么? | 角色名称 | 示例 |
---|---|---|
在表达式中插入代码? | 表达式级宏 | #stringify(value) |
为 struct 自动添加成员? | 成员绑定宏 | @AddID |
生成 computed 属性的 getter/setter? | 属性访问宏 | @UserDefault |
自动生成某个函数体? | 函数体宏 | @AddDescription |
为类型生成协议扩展和默认实现? | 扩展绑定宏 | @CodableSubclass |
额外添加旁路函数或类型? | 对等绑定宏 | @BindEvent |
每个角色背后都对应着一个(或多个)专用协议,用来限制其行为。
6.2 协议是角色的具象化
Swift 宏协议是以 Macro
结尾的一组协议,定义了你在该角色下应该实现的接口。
角色 | 对应协议 | 你要返回的结构类型 |
---|---|---|
表达式级宏 | ExpressionMacro | ExprSyntax |
声明级宏 | DeclarationMacro | [DeclSyntax] |
成员绑定宏 | MemberMacro | [DeclSyntax] |
对等绑定宏 | PeerMacro | [DeclSyntax] |
属性访问宏 | AccessorMacro | [AccessorDeclSyntax] |
扩展绑定宏 | ExtensionMacro | [ExtensionDeclSyntax] |
成员属性宏 | MemberAttributeMacro | [AttributeSyntax] |
函数体宏 | BodyMacro | CodeBlockSyntax |
这些协议都提供了一个 static func expansion(...)
方法,但根据角色不同,返回的语法结构也各不相同。
6.3 建立宏的设计思维路径
宏的本质是 “你想让它为你生成什么代码?” ,这套设计过程可以简化为三步:
你想扩展的目标(角色)
↓
确定宏协议
↓
实现 expansion,构造语法树(行为)
我们将这个过程称为**「角色 → 协议 → 行为」**的思维模型。
例子 1:我想为 struct 添加一个成员 ID
- ⛳ 目标:为 struct 添加成员
- 🎭 角色:成员绑定宏(struct 的成员)
- 📜 协议:
MemberMacro
- 🔧 行为:返回
DeclSyntax
形式的变量声明
@AddID
struct User { }
→ 展开为:
struct User {
var id: String = UUID().uuidString
}
例子 2:我想为属性自动生成访问器(getter/setter)
- ⛳ 目标:替属性添加访问器
- 🎭 角色:属性访问宏
- 📜 协议:
AccessorMacro
- 🔧 行为:返回
[AccessorDeclSyntax]
,如get
和set
@UserDefault("age")
var age: Int
→ 展开为:
get { UserDefaults.standard.integer(forKey: "age") }
set { UserDefaults.standard.set(newValue, forKey: "age") }
例子 3:我想自动为某个函数生成实现体
- ⛳ 目标:添加函数体
- 🎭 角色:函数体宏
- 📜 协议:
BodyMacro
- 🔧 行为:返回
CodeBlockSyntax
@AddDescription
func description() -> String
→ 展开为:
{
return "name=(self.name), age=(self.age)"
}