Swift Macros - 宏之全貌

6 阅读12分钟

1. 宏的定义

Swift 宏(Macro) 是一种在编译期执行的代码生成机制。开发者可以通过简洁的语法标记,在源代码中自动插入、替换或补充逻辑,从而实现样板代码的自动化。

Swift 宏建立在语法树与类型系统之上,具备类型安全语义明确可预测的元编程特性。

宏结构解析.png

为什么使用宏?

Swift 宏的优势体现在以下几个方面:

  • 编译期执行,零运行时开销 宏在编译阶段完成代码展开,避免运行时反射或动态调用的性能负担。
  • 减少样板代码,提升开发效率 无需手动实现 EquatableCodable、监听器等重复性逻辑,宏可以自动生成这些代码。
  • 类型安全,语法无缝衔接 宏展开后的代码与手写代码一样,会经历完整的语法与类型验证,确保可靠性与一致性。

2. 宏的设计原则

Swift 宏的设计秉承“显式、安全、可预测”三大原则,避免“魔法式”的隐式行为:

原则说明
显式调用宏必须通过明确语法标记使用,开发者清晰可见。
类型检查宏生成的代码会经过完整的类型系统验证,不会绕过语言规则。
可预测展开宏的展开逻辑必须是稳定的、可预期的,结果不会因外部环境而改变。

宏不是魔法,它并不神秘,也不凌驾于语言规则之上。你写下的每一个宏调用,都将以可读、可测、可调试的方式插入源代码中。

3. 宏的原理

Swift 宏基于编译器插件(Compiler Plug-in) 机制运行,整个过程发生在编译期,并受到严格的沙盒限制。

宏的展开流程

宏的扩展.png

  1. 提取宏调用:编译器识别源码中的宏语法,并生成对应的原始语法树(Raw Syntax Tree)。
  2. 发送到宏插件:宏语法树被发送至对应插件,该插件在沙盒中以独立进程运行。
  3. 执行宏逻辑:插件处理语法树并生成新的代码片段(语法节点)。
  4. 插入并继续编译:新生成的语法节点被插入原始源码,参与后续的编译过程。

宏的安全性与纯粹性

为了确保宏系统的 安全、稳定与可控性,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)

在绑定宏(例如 MemberMacroAccessorMacro)中,我们通常使用 expanded 方法返回字符串形式的声明代码。但如果不明确命名,编译器将视这些内容为 匿名生成,从而带来几个问题:

  • 无法在语义层面识别生成成员的名称;
  • 代码补全、跳转、文档工具支持不佳;
  • 多个宏同时生成代码时容易发生命名冲突;
  • 其他宏无法可靠地引用这些生成内容。

为了解决这些问题,Swift 引入了 命名说明符 机制,用于精确指定宏生成的实体名称。例如:

 @attached(extension, names: named(==))

这表示:宏将生成一个具名为 == 的成员方法。

命名说明符的种类与用途

命名说明符典型用途原始声明宏生成结果
named("...")设定固定名称struct MyView {}static func makePreview()
prefixed("...")给生成成员加前缀var name: Stringvar 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 宏的分类协议:FreestandingMacroAttachedMacro

Macro 协议的基础上,Swift 将宏分为两类:

  • FreestandingMacro:用于 独立使用的宏,可以直接插入到表达式、声明等任何地方,适合用来生成简单的表达式。
 public protocol FreestandingMacro: Macro { }
  • AttachedMacro:用于 附着在已有代码上的宏,必须绑定到已有的类型、属性、函数等声明上,适合对已有代码进行扩展。
 public protocol AttachedMacro: Macro { }

这两个协议本身不定义任何具体行为,它们为更细分的角色协议提供了基础。

💡 Swift 使用协议体系来设计宏的目的是:

  • 层次清晰:基础协议定义宏的公共行为,高层协议划分宏的使用场景,角色协议定义宏的具体能力。
  • 编译器驱动:根据宏的角色和位置,编译器调用特定协议中的 expansion(...) 方法展开宏。
  • 类型安全:协议方法的定义明确,展开时处理的语法结构与上下文类型都有严格的检查。

5.3 宏的角色协议

每个宏的角色都需要实现一个静态方法 expansion(of:in:),这是编译器在宏展开时调用的核心方法。该方法将接收当前语法节点和上下文信息,并返回生成的语法树,最终插入到用户代码中。

💡 一个宏的实现可以遵循多个协议,从而具备多重角色能力。 例如,以下 AutoCodableMacro 同时实现了 MemberMacroAccessorMacro,因此它具备生成成员和访问器的能力:

 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]
}

功能:为属性添加访问器(如 getsetdidSet 等)。

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 结尾的一组协议,定义了你在该角色下应该实现的接口。

角色对应协议你要返回的结构类型
表达式级宏ExpressionMacroExprSyntax
声明级宏DeclarationMacro[DeclSyntax]
成员绑定宏MemberMacro[DeclSyntax]
对等绑定宏PeerMacro[DeclSyntax]
属性访问宏AccessorMacro[AccessorDeclSyntax]
扩展绑定宏ExtensionMacro[ExtensionDeclSyntax]
成员属性宏MemberAttributeMacro[AttributeSyntax]
函数体宏BodyMacroCodeBlockSyntax

这些协议都提供了一个 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],如 getset
@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)"
}