Highlight
Swift 宏通过角色(Role)系统约束宏的能力边界,提供独立宏(
#)和附属宏(@)两大类共七种角色,让开发者在编译期安全地生成代码,同时保持输入类型检查、展开可预测、结果可调试。
核心内容
Swift 已经有很多自动生成代码的特性:Codable 自动合成 CodingKeys 和编解码方法,Result Builder 把声明式语法展开成命令式代码。但这些特性都硬编码在编译器里,想要新增类似能力必须修改编译器源码。
宏把这个能力开放给了开发者。你可以创建自己的”语言特性”,以 Swift Package 的形式分发,不需要 fork 编译器。
(00:50)
四大设计原则
Swift 宏的设计遵循四个核心目标:
显式标识。独立宏以 # 开头,附属宏以 @ 开头。没有看到这两个符号,就可以确定没有宏参与。这让代码阅读者有明确的预期。
类型安全。传入宏的参数必须是完整的、类型正确的表达式。#unwrap(1 + ) 会直接报语法错误,@AddCompletionHandler(parameterName: 42) 会报类型不匹配。宏的输入和输出都经过完整的类型检查。
可预测展开。宏只能添加代码,不能删除或修改已有代码。你不需要担心某个宏会偷偷删掉你的函数调用。
透明可调试。Xcode 支持右键查看宏展开结果、在展开代码中设断点、单步调试。即使宏来自闭源库,这些工具照样工作。
(03:16)
独立宏
独立宏用 # 调用,分两种角色:
表达式宏(@freestanding(expression))展开成一个表达式。#stringify(a + b) 展开成 (a + b, "a + b"),可以在任何需要表达式的地方使用。
声明宏(@freestanding(declaration, names: arbitrary))展开成一个或多个声明。比如 #makeArrayND(n: 5) 可以展开成 Array5D 结构体的完整定义。
(04:51)
附属宏
附属宏用 @ 标注在声明上,分五种角色:
peer(@attached(peer, names: overloaded))在目标声明旁边添加新的声明。@AddCompletionHandler 可以为 async 函数自动生成一个带 completion handler 的重载版本。
@AddCompletionHandler(parameterName: "onCompletion")
func fetchAvatar(_ username: String) async -> Image? {
// ...
}
// 宏展开后自动生成:
func fetchAvatar(
_ username: String,
onCompletion: @escaping (Image?) -> Void
) {
Task.detached {
onCompletion(await fetchAvatar(username))
}
}
accessor(@attached(accessor))为存储属性添加 getter/setter。@DictionaryStorage 可以把属性的读写自动代理到字典中。
memberAttribute(@attached(memberAttribute))为类型的成员添加属性。@DictionaryStorage 标注在结构体上时,会自动为每个属性添加 @DictionaryStorage。
member(@attached(member, names: named(dictionary), named(init(dictionary:))))为类型添加新成员。@DictionaryStorage 会自动生成 dictionary 属性和 init(dictionary:) 构造器。
conformance(@attached(conformance))为类型添加协议遵循。@DictionaryStorage 会自动让结构体遵循 DictionaryRepresentable 协议。
一个宏可以同时拥有多个角色。@DictionaryStorage 就同时实现了 accessor、memberAttribute、member 和 conformance 四种角色,把原本需要写几十行的样板代码压缩成一行标注。
(11:23)
实现机制
宏的实现写在独立的编译器插件中。声明用 #externalMacro 关联到实现:
@freestanding(expression)
macro stringify<T>(_ expr: T) -> (T, String) = #externalMacro(
module: "MyLibMacros",
type: "StringifyMacro"
)
实现需要导入 SwiftSyntax、SwiftSyntaxMacros 和 SwiftSyntaxBuilder:
import SwiftSyntax
import SwiftSyntaxMacros
import SwiftSyntaxBuilder
struct DictionaryStorageMacro: MemberMacro {
static func expansion(
of attribute: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
return [
"init(dictionary: [String: Any]) { self.dictionary = dictionary }",
"var dictionary: [String: Any]"
]
}
}
(18:01)
错误报告
宏可以通过 context.diagnose() 向编译器报告错误:
struct DictionaryStorageMacro: MemberMacro {
static func expansion(
of attribute: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard declaration.is(StructDeclSyntax.self) else {
let structError = Diagnostic(
node: attribute,
message: MyLibDiagnostic.notAStruct
)
context.diagnose(structError)
return []
}
return [
"init(dictionary: [String: Any]) { self.dictionary = dictionary }",
"var dictionary: [String: Any]"
]
}
}
enum MyLibDiagnostic: String, DiagnosticMessage {
case notAStruct
var severity: DiagnosticSeverity { return .error }
var message: String {
switch self {
case .notAStruct:
return "'@DictionaryStorage' can only be applied to a 'struct'"
}
}
var diagnosticID: MessageID {
MessageID(domain: "MyLibMacros", id: rawValue)
}
}
这样当开发者把 @DictionaryStorage 用在枚举上时,Xcode 会直接显示错误提示。
(25:17)
命名声明
宏展开中生成的新声明需要在宏的 names: 参数中预告。这帮助编译器在宏展开前就了解程序结构:
named(xxx)— 生成名为 xxx 的声明overloaded— 生成与目标同名的重载声明arbitrary— 生成任意名称的声明(需要显式声明)
@attached(conformance)
@attached(member, names: named(dictionary), named(init(dictionary:)))
@attached(memberAttribute)
@attached(accessor)
macro DictionaryStorage(key: String? = nil)
@attached(peer, names: overloaded)
macro AddCompletionHandler(parameterName: String = "completionHandler")
@freestanding(declaration, names: arbitrary)
macro makeArrayND(n: Int)
(35:44)
详细内容
#unwrap 宏的实现
#unwrap 是一个实用的表达式宏,把危险的 ! 强制解包替换成带详细错误信息的 preconditionFailure:
let image = #unwrap(downloadedImage, message: "was already checked")
// 展开后:
{ [downloadedImage] in
guard let downloadedImage else {
preconditionFailure(
"Unexpectedly found nil: 'downloadedImage' was already checked",
file: "main/ImageLoader.swift",
line: 42
)
}
return downloadedImage
}()
实现时需要处理几个细节:
- 捕获变量名可能与外部作用域冲突,需要用
context.makeUniqueName()生成唯一名称 - 错误信息中要包含原始表达式的描述,用
originalWrapped.description - 文件和行号通过
context.location(of:)获取
static func makeGuardStmt(
wrapped: TokenSyntax,
originalWrapped: ExprSyntax,
message: ExprSyntax,
in context: some MacroExpansionContext
) -> StmtSyntax {
let messagePrefix = "Unexpectedly found nil: '\(originalWrapped.description)' "
let originalLoc = context.location(of: originalWrapped)!
return """
guard let \(wrapped) else {
preconditionFailure(
\(literal: messagePrefix) + \(message),
file: \(originalLoc.file),
line: \(originalLoc.line)
)
}
"""
}
关键点:
\(literal: messagePrefix)把字符串作为字面量插入,避免转义问题context.location(of:)返回原始表达式的源码位置context.makeUniqueName()生成不会与外部变量冲突的唯一标识符
(30:17)
@DictionaryStorage 的完整演化
@DictionaryStorage 展示了如何组合多个角色来逐步简化代码。
原始代码:
struct Person: DictionaryRepresentable {
init(dictionary: [String: Any]) { self.dictionary = dictionary }
var dictionary: [String: Any]
var name: String {
get { dictionary["name"]! as! String }
set { dictionary["name"] = newValue }
}
var height: Measurement<UnitLength> {
get { dictionary["height"]! as! Measurement<UnitLength> }
set { dictionary["height"] = newValue }
}
var birthDate: Date? {
get { dictionary["birth_date"] as! Date? }
set { dictionary["birth_date"] = newValue as Any? }
}
}
第一步:用 accessor 角色为每个属性添加 getter/setter:
struct Person: DictionaryRepresentable {
init(dictionary: [String: Any]) { self.dictionary = dictionary }
var dictionary: [String: Any]
@DictionaryStorage var name: String
@DictionaryStorage var height: Measurement<UnitLength>
@DictionaryStorage(key: "birth_date") var birthDate: Date?
}
第二步:加上 memberAttribute 角色,让结构体上的 @DictionaryStorage 自动为每个属性添加 @DictionaryStorage:
@DictionaryStorage
struct Person: DictionaryRepresentable {
init(dictionary: [String: Any]) { self.dictionary = dictionary }
var dictionary: [String: Any]
var name: String
var height: Measurement<UnitLength>
@DictionaryStorage(key: "birth_date") var birthDate: Date?
}
第三步:加上 member 角色,自动生成 init(dictionary:) 和 dictionary 属性:
@DictionaryStorage
struct Person {
var name: String
var height: Measurement<UnitLength>
@DictionaryStorage(key: "birth_date") var birthDate: Date?
}
第四步:加上 conformance 角色,自动遵循 DictionaryRepresentable:
@DictionaryStorage
struct Person {
var name: String
var height: Measurement<UnitLength>
@DictionaryStorage(key: "birth_date") var birthDate: Date?
}
原本 20 多行的样板代码,最终压缩成 5 行。
(17:28)
测试宏
SwiftSyntaxMacrosTestSupport 提供了 assertMacroExpansion:
import MyLibMacros
import XCTest
import SwiftSyntaxMacrosTestSupport
final class MyLibTests: XCTestCase {
func testMacro() {
assertMacroExpansion(
"""
@DictionaryStorage var name: String
""",
expandedSource: """
var name: String {
get { dictionary["name"]! as! String }
set { dictionary["name"] = newValue }
}
""",
macros: ["DictionaryStorage": DictionaryStorageMacro.self]
)
}
}
(38:28)
核心启发
自动为异步函数生成回调版本
@AddCompletionHandler 的思路可以扩展。很多项目需要同时维护 async 版本和回调版本。写一个宏自动从 async 函数生成 completion handler 版本,可以消除大量重复代码。
入口:@attached(peer, names: overloaded)。读取函数的参数和返回类型,生成一个调用原函数并在 Task 中 await 结果的新函数。
自动生成 Equatable/Hashable 的自定义实现
Swift 自动合成的 Equatable 有时不够用。可以写一个 @CustomEquatable 宏,根据属性标记决定哪些字段参与比较、如何比较。
入口:@attached(member) + MemberMacro。遍历属性,为标记了 @CompareBy 的属性生成对应的比较逻辑。
为 SwiftUI 生成预览代码
写一个 @PreviewVariants 宏,标注在 View 上,自动生成多个预览变体(不同尺寸、不同颜色模式、不同动态字体)。
入口:@attached(member)。读取 View 类型,生成多个 PreviewProvider 实现。
自动生成 Builder 模式代码
很多类需要 Builder 模式来构造复杂对象。写一个 @Builder 宏,为类自动生成对应的 Builder 类和链式调用方法。
入口:@attached(peer)。读取类的所有属性,生成一个同名的 Builder 类,每个属性对应一个 withXxx() 方法和一个 build() 方法。
关联 Session
- Write Swift macros — 动手编写第一个 Swift 宏
- Generalize APIs with parameter packs — Swift 泛型编程的高级特性
- Swift concurrency: Behind the scenes — Swift 结构化并发的底层原理
- Meet Swift OpenAPI Generator — 用代码生成简化服务端 API 调用
评论
GitHub Issues · utterances