Swift 宏的强大源于其背后一套精巧严谨的协议体系。这些协议定义了:
- 宏的行为规范:如何与编译器通信,如何生成语法树
- 宏的能力边界:什么宏可以插入什么样的结构
- 宏的输入输出约束:需要接受什么样的输入,返回什么样的输出
在 Swift 中, “宏 = 协议方法的实现” 。宏不会在运行时参与逻辑,而是在编译期间将协议方法转换为结构化代码。
本篇将深入解析这些协议的共性特征与调用方式,为你在后续实现各种角色宏打下统一的基础。
Swift 宏协议的共性特征
Swift 宏虽然分工明确(表达式宏、声明宏、成员宏等),但它们的实现方式高度统一,主要体现为以下特征:
编号 |
特征 |
描述 |
1 |
方法统一命名为 expansion
|
所有宏协议都实现 static func expansion(...) 作为展开主入口。 |
2 |
支持 throws 异常机制 |
展开过程中可中止并抛出诊断错误。 |
3 |
必带 context 参数 |
提供编译期上下文信息,是宏的“工具箱”。 |
4 |
必带 node 参数 |
表示宏的调用现场,如 #宏名(...) 或 @宏名 。 |
5 |
输入输出皆为 Syntax 类型 |
宏只操作语法树,输入输出都是 SwiftSyntax 节点。 |
6 |
仅在编译期执行 |
宏不能访问运行时信息,所有逻辑基于静态源码。 |
7 |
返回类型严格固定 |
每种宏角色返回类型不同,且不可交叉使用。 |
1. 所有宏都实现 static func expansion(...)
Swift 宏协议统一使用 expansion
方法命名,使得不同类型的宏拥有相似的签名与调用习惯,极大降低学习与维护成本。
// 各协议方法签名示例
protocol ExpressionMacro {
static func expansion(...) throws -> ExprSyntax
}
protocol DeclarationMacro {
static func expansion(...) throws -> [DeclSyntax]
}
- 方法总是
static
,因为宏不依赖实例
- 输入是调用现场
node
+ 编译上下文 context
- 输出是结构化语法树,如
ExprSyntax
、DeclSyntax
等
2. 宏支持 throws
,可中止并报告错误
所有宏的 expansion
方法都支持 throws
,允许在发现语义错误时立即中止,并通过 context.diagnose(...)
抛出诊断信息,提升宏的可维护性与用户友好度。

只需要在适当的地方抛出异常,你可以自行编辑异常的message,以便使用者更好的理解该异常。
public struct StringifyMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> ExprSyntax {
throw ASTError("错误提示: the macro does not have any arguments")
}
}
你可以通过自定义错误类型(如 ASTError
)提供清晰的人类可读信息,IDE 也会高亮定位到宏调用位置,提升调试体验。
3. context
宏的工具箱
每个宏都会收到一个 context
参数(类型为 some MacroExpansionContext
),这是宏与编译器交互的主要手段,具备多项能力:
public protocol MacroExpansionContext: AnyObject {
func makeUniqueName(_ name: String) -> TokenSyntax
func diagnose(_ diagnostic: Diagnostic)
func location(of node: some SyntaxProtocol, at position: PositionInSyntaxNode, filePathMode: SourceLocationFilePathMode) -> AbstractSourceLocation?
var lexicalContext: [Syntax] { get }
}
它是宏与编译器沟通的桥梁,也是实现宏逻辑动态化的关键接口。以下是 Swift 宏系统中 MacroExpansionContext
协议四个核心成员的作用详解,按重要性分层说明:
3.1 命名避冲突:makeUniqueName(_:)
自动生成唯一标识符,避免命名冲突
// 使用场景:临时变量、缓存值、内部标识符等场景。
let uniqueVar = context.makeUniqueName("result")
// 输出结果可能是 `result_7FE3A1` 之类的唯一名称
3.2 诊断报告:diagnose(_:)
核心作用:编译时错误报告系统
-
多级诊断:支持 error / warning / note 三种严重级别
-
精准定位:关联到具体语法节点(如高亮错误位置)
-
修复建议:可附加自动修复方案(FixIt)
public struct StringifyMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> ExprSyntax {
context.diagnose(Diagnostic(node: node, message: MacroDiagnostic.deprecatedUsage))
throw ASTError("错误提示: xxxxxx")
}
}
某些宏过期时,可以通过 context.diagnose(...)
给于警告提醒。

DiagnosticMessage
这里的 Diagnostic.message
需要一个实现 DiagnosticMessage
协议的实例。
public protocol DiagnosticMessage: Sendable {
/// The diagnostic message that should be displayed in the client.
var message: String { get }
/// See ``MessageID``.
var diagnosticID: MessageID { get }
var severity: DiagnosticSeverity { get }
}
3.3 源码定位:location(of:at:filePathMode:)
可定位到调用宏的具体源代码行列,便于诊断、代码导航、日志标注等用途:
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> ExprSyntax {
let loc = context.location(of: node, at: .afterLeadingTrivia, filePathMode: .fileID )
......
}
func location( of node: some SyntaxProtocol, at position: PositionInSyntaxNode, filePathMode: SourceLocationFilePathMode ) -> AbstractSourceLocation?
从 AbstractSourceLocation 返回值中,可以获取以下信息:
public struct AbstractSourceLocation: Sendable {
/// 文件位置
public let file: ExprSyntax
/// 行的位置
public let line: ExprSyntax
/// 字符位置
public let column: ExprSyntax
3.4 词法作用域追踪:lexicalContext
核心作用:获取词法作用域上下文
以数组形式,记录从当前节点向外的层层包裹结构;
经过脱敏处理(如移除函数体、清空成员列表)。
// 检查是否在类方法中
let isInClassMethod = context.lexicalContext.contains {
$0.is(FunctionDeclSyntax.self) &&
$0.parent?.is(ClassDeclSyntax.self) != nil
}
4. node
调用现场信息
每个宏的 expansion
方法,除了 context
外,还会接收一个 node
参数,类型通常是 some SyntaxProtocol
(如 FreestandingMacroExpansionSyntax
、AttributeSyntax
等)。
它代表了宏的调用现场——也就是源码中触发宏展开的那段语法结构。
简单理解:node
就是“#宏名(...)”或“@宏名” 这一整段的解析结果。
以自由宏为例,node
类型通常是 FreestandingMacroExpansionSyntax
,它包含了调用宏时的所有组成元素:
public protocol FreestandingMacroExpansionSyntax: SyntaxProtocol {
var pound: TokenSyntax { get set } // "#" 符号
var macroName: TokenSyntax { get set } // 宏名
var genericArgumentClause: GenericArgumentClauseSyntax? { get set } // 泛型参数
var leftParen: TokenSyntax? { get set } // 左括号 "("
var arguments: LabeledExprListSyntax { get set } // 参数列表
var rightParen: TokenSyntax? { get set } // 右括号 ")"
var trailingClosure: ClosureExprSyntax? { get set } // 尾随闭包
var additionalTrailingClosures: MultipleTrailingClosureElementListSyntax { get set } // 多个尾随闭包
}
具体能做什么?
通过解析 node
,可以在宏内部获取宏调用时传递的信息,从而进行自定义生成:
-
提取参数:解析
arguments
,得到用户传入的内容;
-
读取宏名:从
macroName
获取调用者使用的名字(有些宏支持重名扩展);
-
处理泛型:如果
genericArgumentClause
存在,可以根据泛型参数生成不同代码;
-
解析闭包:支持分析和利用用户传递的尾随闭包;
-
实现自定义行为:比如根据传入参数数量、类型、值,决定生成什么样的代码。
示例
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> ExprSyntax {
// 取出第一个参数
guard let firstArg = node.arguments.first?.expression else {
throw ASTError("缺少参数")
}
// 根据参数生成不同表达式
return "print((firstArg))"
}
小结: node
= 宏调用时的源码快照, context
= 辅助功能工具箱。
两者结合使用,才能让宏既能理解调用现场,又能灵活地生成对应代码。
5. 输入输出皆基于 Syntax
节点
Swift 宏以结构化 AST(抽象语法树)为基础,输入输出都基于 SwiftSyntax
类型,例如:
- 输入:
AttributeSyntax
、FreestandingMacroExpansionSyntax
、DeclSyntaxProtocol
;
- 输出:
ExprSyntax
、[DeclSyntax]
、[AccessorDeclSyntax]
等。
这种设计保证了宏生成的代码具备:
- 与手写代码一致的结构完整性;
- 良好的可分析性与可重构性;
- 自动享受 IDE 语法高亮、错误检测等支持。
Swift 宏不是简单拼接字符串,而是真正生成 AST。
6. 宏只运行于编译时
Swift 宏只能在编译期运行,这意味着它们不能访问运行时信息、全局变量、实例状态或外部服务。所有宏的行为都必须建立在静态源代码、类型系统和语法结构之上。
这为宏提供了如下保证:
-
可预测性:展开结果与运行环境无关,确保行为一致;
-
可分析性:工具链可以分析宏行为,进行语法检查与补全;
-
可维护性:宏代码不会隐藏运行时副作用,有利于重构和测试。
开发者在编写宏时,也应遵循“编译时思维”,尽可能将逻辑转化为静态分析与结构转换。
7. 每种宏的返回类型固定
每个宏协议都明确限定了其 expansion
方法的返回类型,这种限制具有强约束力:
宏协议 |
返回类型 |
ExpressionMacro |
ExprSyntax |
DeclarationMacro |
[DeclSyntax] |
MemberMacro |
[DeclSyntax] |
AccessorMacro |
[AccessorDeclSyntax] |
BodyMacro |
[CodeBlockItemSyntax] |
ExtensionMacro |
[ExtensionDeclSyntax] |
MemberAttributeMacro |
[AttributeSyntax] |
这种强约束带来:
- 类型安全;
- 生成结果合法;
- 避免不同宏角色混淆使用。
比如:成员宏只能生成成员声明,不能直接生成表达式或代码块。
总结
Swift 宏协议的结构化设计,使得宏具备了安全、清晰、灵活的特性。无论你编写哪种类型的宏,理解 expansion
的统一调用模式、context
工具箱能力、node
的语法抽象、以及 Syntax
类型的输入输出机制,都是构建可靠宏逻辑的基础。
在接下来的章节中,我们将深入每一种宏协议(如 ExpressionMacro
、DeclarationMacro
等),并结合实际案例,帮助你实现更多有趣且实用的 Swift 宏。