Swift 宏(Macro)入门趣谈(五)
概述
苹果在去年 WWDC 23 中就为 Swift 语言新增了“其利断金”的重要小伙伴 Swift 宏(Swift Macro)。为此,苹果特地用 2 段视频(入门和进阶)颇为隆重的介绍了它。
那么到底 Swift 宏是什么?有什么用?它和 C/C++ 语言中的宏又有什么异同呢?本系列博文将会尝试为小伙伴们揭开 Swift 宏的神秘面纱。
在本篇博文中,您将学到如下内容:
- 概述
- 5.4 完成宏主体
- 5.5 验证宏展开后的结果
- 6. 另一个简洁的解决方案
- 7. 目前 Swift 宏的不足之处
- 总结
相信学完本系列博文后,Swift Macro 会从大家心中的“阳春白雪”变为“阳阿薤露”,小伙伴们必可以将它们运用的“如臂使指”。
那还等什么呢?Let‘s go!!!😉
5.4 完成宏主体
在完成了 @nilable 宏接口、客户端代码以及宏主体初步结构的搭建之后,现在回到 NilableMacro 结构的定义中,我们现在可以尝试补全 expansion() 方法中所有展开代码了。
不过在撸码之前,我们有必要先来了解一下 expansion() 方法传入实参所扮演的角色。在这个例子中,我们只关心其中的 declaration 参数,它是一个 some DeclSyntaxProtocol 类型:
providingPeersOf declaration: some DeclSyntaxProtocol
在 Xcode 调试控制台中,我们利用 po 命令可以列出它的结构细节:
FunctionDeclSyntax
├─attributes: AttributeListSyntax
│ ╰─[0]: AttributeSyntax
│ ├─atSign: atSign
│ ╰─attributeName: IdentifierTypeSyntax
│ ╰─name: identifier("nilable")
├─modifiers: DeclModifierListSyntax
│ ├─[0]: DeclModifierSyntax
│ │ ╰─name: keyword(SwiftSyntax.Keyword.static)
│ ╰─[1]: DeclModifierSyntax
│ ╰─name: keyword(SwiftSyntax.Keyword.private)
├─funcKeyword: keyword(SwiftSyntax.Keyword.func)
├─name: identifier("test")
├─genericParameterClause: GenericParameterClauseSyntax
│ ├─leftAngle: leftAngle
│ ├─parameters: GenericParameterListSyntax
│ │ ├─[0]: GenericParameterSyntax
│ │ │ ├─attributes: AttributeListSyntax
│ │ │ ├─name: identifier("Root")
│ │ │ ╰─trailingComma: comma
│ │ ╰─[1]: GenericParameterSyntax
│ │ ├─attributes: AttributeListSyntax
│ │ ├─name: identifier("Value")
│ │ ├─colon: colon
│ │ ╰─inheritedType: IdentifierTypeSyntax
│ │ ╰─name: identifier("Comparable")
│ ╰─rightAngle: rightAngle
├─signature: FunctionSignatureSyntax
│ ├─parameterClause: FunctionParameterClauseSyntax
│ │ ├─leftParen: leftParen
│ │ ├─parameters: FunctionParameterListSyntax
│ │ │ ├─[0]: FunctionParameterSyntax
│ │ │ │ ├─attributes: AttributeListSyntax
│ │ │ │ ├─modifiers: DeclModifierListSyntax
│ │ │ │ ├─firstName: identifier("dump")
│ │ │ │ ├─secondName: identifier("a")
│ │ │ │ ├─colon: colon
│ │ │ │ ├─type: IdentifierTypeSyntax
│ │ │ │ │ ├─name: identifier("KeyPath")
│ │ │ │ │ ╰─genericArgumentClause: GenericArgumentClauseSyntax
│ │ │ │ │ ├─leftAngle: leftAngle
│ │ │ │ │ ├─arguments: GenericArgumentListSyntax
│ │ │ │ │ │ ├─[0]: GenericArgumentSyntax
│ │ │ │ │ │ │ ├─argument: IdentifierTypeSyntax
│ │ │ │ │ │ │ │ ╰─name: identifier("Root")
│ │ │ │ │ │ │ ╰─trailingComma: comma
│ │ │ │ │ │ ╰─[1]: GenericArgumentSyntax
│ │ │ │ │ │ ╰─argument: IdentifierTypeSyntax
│ │ │ │ │ │ ╰─name: identifier("Value")
│ │ │ │ │ ╰─rightAngle: rightAngle
│ │ │ │ ╰─trailingComma: comma
│ │ │ ├─[1]: FunctionParameterSyntax
│ │ │ │ ├─attributes: AttributeListSyntax
│ │ │ │ ├─modifiers: DeclModifierListSyntax
│ │ │ │ ├─firstName: identifier("b")
│ │ │ │ ├─colon: colon
│ │ │ │ ├─type: IdentifierTypeSyntax
│ │ │ │ │ ╰─name: identifier("String")
│ │ │ │ ╰─trailingComma: comma
│ │ │ ╰─[2]: FunctionParameterSyntax
│ │ │ ├─attributes: AttributeListSyntax
│ │ │ ├─modifiers: DeclModifierListSyntax
│ │ │ ├─firstName: identifier("c")
│ │ │ ├─colon: colon
│ │ │ ╰─type: IdentifierTypeSyntax
│ │ │ ╰─name: identifier("Int")
│ │ ╰─rightParen: rightParen
│ ├─effectSpecifiers: FunctionEffectSpecifiersSyntax
│ │ ╰─throwsClause: ThrowsClauseSyntax
│ │ ╰─throwsSpecifier: keyword(SwiftSyntax.Keyword.throws)
│ ╰─returnClause: ReturnClauseSyntax
│ ├─arrow: arrow
│ ╰─type: IdentifierTypeSyntax
│ ╰─name: identifier("Bool")
╰─body: CodeBlockSyntax
├─leftBrace: leftBrace
├─statements: CodeBlockItemListSyntax
│ ╰─[0]: CodeBlockItemSyntax
│ ╰─item: PrefixOperatorExprSyntax
│ ├─operator: prefixOperator("!")
│ ╰─expression: MemberAccessExprSyntax
│ ├─base: DeclReferenceExprSyntax
│ │ ╰─baseName: identifier("b")
│ ├─period: period
│ ╰─declName: DeclReferenceExprSyntax
│ ╰─baseName: identifier("isEmpty")
╰─rightBrace: rightBrace
代码看起来很长,似乎有点儿“一望无际”。不过如果我们查看它的 description 属性就会恍然大悟:它其实就是 @nilable 宏修饰方法所对应的语法树。
所以基本上来说,我们只需要将 declaration 的内容“修剪”为想要的结果即可。对于我们这个例子,就是将 declaration 中第一个 KeyPath 参数中的 Value 变为可选类型 Value?。
下面是我们第一种实现,因为考虑到了“调皮的”用户可能犯的各种错误,所以比较冗长。不过别急,后面我们会给出精简后的方案:
public static func expansion(of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [DeclSyntax] {
guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else {
throw MacroExpansionErrorMessage("必须在方法上使用我哦!")
}
let funcName = funcDecl.name
let funcModifiers = funcDecl.modifiers
let funcGenericParameterClause = funcDecl.genericParameterClause
let funcEffecSpecifier = funcDecl.signature.effectSpecifiers?.description ?? ""
let funcReturnClause = funcDecl.signature.returnClause?.description ?? ""
let funcBody = funcDecl.body
guard let funcFirstParamenter = funcDecl.signature.parameterClause.parameters.first else {
throw MacroExpansionErrorMessage("方法必须至少要有 1 个参数哦!")
}
guard let funcFirstParamenterType = funcFirstParamenter.type.as(IdentifierTypeSyntax.self) else {
throw MacroExpansionErrorMessage("方法参数格式错误哦!")
}
guard funcFirstParamenterType.name.description == "KeyPath" else {
throw MacroExpansionErrorMessage("方法的第一个参数必须是 KeyPath 类型哦!")
}
guard let secondArgment = funcFirstParamenterType.genericArgumentClause?.arguments.last, secondArgment.argument.as(OptionalTypeSyntax.self) == nil else {
throw MacroExpansionErrorMessage("方法 KeyPath 参数中的 Value 不是 Optional 类型哦!")
}
let firstName = funcFirstParamenter.firstName.description
var firstArg = if let secondName = funcFirstParamenter.secondName?.description {
"\(firstName)\(secondName): "
} else {
"\(firstName): "
}
let rootName = funcFirstParamenterType.genericArgumentClause!.arguments.first!.argument.as(IdentifierTypeSyntax.self)!.name.description
let valueName = secondArgment.argument.as(IdentifierTypeSyntax.self)!.description
firstArg += "KeyPath<\(rootName), \(valueName)?>"
let parameters = funcDecl.signature.parameterClause.parameters
var otherParametersDesc = ""
if parameters.count > 1 {
for parameter in parameters.dropFirst() {
otherParametersDesc += "\(parameter.description)"
}
}
let nilFuncDecl = try FunctionDeclSyntax("\(funcModifiers)func \(funcName)\(funcGenericParameterClause)(\(raw: firstArg), \(raw: otherParametersDesc)) \(raw: funcEffecSpecifier)\(raw: funcReturnClause) \(funcBody)")
return [.init(nilFuncDecl)]
}
在上面的代码中,我们做了这样几件事:
- 为用户在构造宏所修饰的表达式时可能犯的各种错误(比如用户在属性而不是方法上应用 @nilable 宏)提供贴心的提示;
- 确定 KeyPath 中 Value 的名称;
- 考虑到 KeyPath 参数可能出现的名称前缀;
- 确定方法签名中的其它信息(比如其它参数类型和返回类型);
- 考虑到方法本身可能存在一些 modifier 和 effectSpecifiers 修饰器(比如 static、private、throws 等);
5.5 验证宏展开后的结果
回到 main.swift 源代码文件中,在选中 @nilable 宏关键字后将其展开,如果不出意外我们应该可以正确创建 sortItemsBy 排序方法的 KeyPath 可选 Value 版本(Value?):
这意味着,下面这些代码都可以顺利编译通过了:
let itemsByName = try! model.sortItemsBy(keyPath: \.name)
let itmesByNickname = try! model.sortItemsBy(keyPath: \.nickname)
棒棒哒!成就感爆棚的小伙伴们赶快给自已一个大大的赞吧!👍🏻
其实验证 Swift 宏展开结果最好的方法是使用单元测试,但由于目前宏单元测试略显“呆滞和笨拙”,所以暂时略去不表。
6. 另一个简洁的解决方案
虽然我们解决了问题,但上述实现实在是有点“臃肿不堪”。
让我们马上再做一次头脑风暴:我们实际只是想修改 sortItemsBy() 方法 KeyPath 形参中的 Value,如果只考虑几种可能出现的错误,仅需一个简单的字符串内容替换操作即可大功告成!
下面是我们 expansion() 方法简化后的新实现:
public static func expansion(of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [DeclSyntax] {
guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else {
throw MacroExpansionErrorMessage("必须在方法上使用我哦!")
}
var funcDesc = funcDecl.description
funcDesc.replace(/@nilable/, with: "")
let valueFinder = /.*KeyPath<.+?,\s*(.+)>,/
guard let value = try valueFinder.firstMatch(in: funcDesc)?.output.1 else {
throw MacroExpansionErrorMessage("找不到 KeyPath !")
}
funcDesc.replaceSubrange(value.startIndex..<value.endIndex, with: "\(value)?")
return [.init(try FunctionDeclSyntax("\(raw: funcDesc)"))]
}
在上面的代码中,我们只是仅仅替换原方法里形参 KeyPath 中的值类型 Value 为 Value? 而已,真是简单的不要不要的。
验证 @nilable 宏展开分结果,和之前是一毛一样的,棒棒哒!💯
7. 目前 Swift 宏的不足之处
虽然 Swift Macros 更好地弥补了 Swift 语言动态性不足的“先天缺陷”,让秃头码农们更优雅的遵循 “KISS” 原则,避免了代码重复。
但是,目前 Swift 宏仍有一些白圭之玷,它们主要表现在如下几个方面:
- 无论我们是否喜欢,即使一个非常简单的宏实现都需要放在 Swift Package 中;
- 官方示例很少,研究起来比较容易掉头发;
- 定义 7 种宏的“隐藏”选项几乎没有文档,更容易掉头发;
- FunctionDeclSyntax 等一些声明语法实体都是只读的,不太灵活;
- 虽然像 FunctionDeclSyntax 这些声明语法实体都包含 ResultBuilder 构造器,但实际无法像 SwiftUI 那样自由组合内部元素;
- 因为要实现庞大 Swift 语言的类型安全,所以 Swift 宏背后语法树的结构也很复杂,经常云里雾里的感觉;
- 貌似宏的实现有缓存,所以在 Xcode 16 中修改宏的展开代码有时不能及时触发测试用例中宏展开结果的刷新,需要重启 Xcode;
以上只是其中一部分“美中不足”,其它一些小问题并没有完全列出来。
其实,现在 Swift 宏最大的问题就是它缺乏文档而且太复杂了,这就是为什么目前没什么人用的原因。
希望苹果在 WWDC 25 的 Swift Macros 2.0 中(如果可能的话)可以简化和改善它。
即便如此,雪中送炭的 Swift 宏仍然给了我们太多惊喜和便利,值得小伙伴们进一步深入挖掘。
本系列文章至此告一段落了,有机会我们会单写几篇 Swift 宏的进阶博文,敬请期待吧!
总结
在本篇博文中,我们介绍了宏展开方法中 declaration 参数的构成(FunctionDeclSyntax ),并详细讨论了自定义宏主体的实现;我们随后还精简了宏的展开逻辑并顺便聊了聊当前 Swift 宏的一些不足之处。
感谢观赏,再会啦!😎