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 } }
message:诊断信息的信息
diagnosticID:诊断 ID
severity:诊断严重程度public enum DiagnosticSeverity { case error // 编译错误,阻止构建。 case warning // 编译警告,不阻止构建。 case note // 提示信息,常用于补充说明。 }
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
-
四种定位模式:
enum PositionInSyntaxNode { case beforeLeadingTrivia // 包含注释/空格 case afterLeadingTrivia // 实际代码起始处 case beforeTrailingTrivia // 实际代码结束处 case afterTrailingTrivia // 包含尾部注释 } -
路径显示控制:
.fileID→"ModuleName/FileName.swift"(安全格式).filePath→ 完整系统路径(调试用)
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 宏。