Swift 5.9 Macros 有哪些新更新
前言
虽然 Swift 6 已经在地平线上浮现,但 5.x 版本仍然有很多新功能-更简单的 if 和 switch 用法、宏、非可复制类型、自定义 actor 执行器等等都将在 Swift 5.9 中推出,再次带来了一个巨大的更新。
Macros(宏)
Macros(宏)在 Swift 中被引入,其中 SE-0382、SE-0389 和 SE-0397 结合起来,允许我们在编译时创建能够转换语法的代码。
在像 C++ 这样的语言中,宏是一种对代码进行预处理的方式,可以在代码被主编译器看到之前对其进行文本替换,从而生成那些你不想手动编写的代码。
Swift 的宏类似,但功能更强大,因此也更加复杂。还允许我们在编译前动态操作项目的 Swift 代码,从而在编译时注入额外的功能。
需要了解的关键信息
宏是安全的类型,不仅仅是简单的字符串替换,因此需要准确告诉宏它将处理的数据。
在构建阶段作为外部程序运行,并且不属于主应用目标。
宏被分解为多个较小的类型,例如 ExpressionMacro 用于生成单个表达式,AccessorMacro 用于添加 getter和setter,ConformanceMacro 用于使类型符合某个协议。
宏与解析后的源代码一起工作,可以查询代码的各个部分,例如正在操作的属性的名称、类型,或者结构体内部的各种属性。
宏在一个沙盒中工作,只能在给定的数据上操作。
最后一个部分最重要,Swift 的宏支持是基于 Apple 的 SwiftSyntax 库构建的,用于理解和操作源代码。必须将其作为宏的依赖项添加到项目中。
环境准备
了解完 Swift 宏的基本信息之后,终于我们可以进入到实战环节了,工欲善其事必先利其器,首先我们需要做好以下准备
- macOS Ventura 13.3 以上操作系统
- Xcode 15 以上,本文使用的版本是15.0 beta (15A5160n)
- Swift 入门级语法(掌握 Hello World 的 4 种写法?)
然后我们需要初始化一个 Swift 宏开发工程。
1、直接打开 Xcode 15,File -> New -> Package
2、选择 Swift Macro 模版,然后给我们的工程起一个名字,我这里就叫做 “SwiftMacroKit”
3、打开工程,大功告成
这是一个 SPM 管理的工程,如果打开 Package 文件,我们可以看到它依赖了前面提到的 SwiftSyntax 库
dependencies: [
// Depend on the latest Swift 5.9 prerelease of SwiftSyntax
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0-swift-5.9-DEVELOPMENT-SNAPSHOT-2023-04-25-b"),
],
除此之外,工程其实还依赖了两个库, SwiftSyntaxBuilder 和 SwiftSyntaxMacros ,这三个库的职责分别是
- SwiftSyntax:提供 Swift 语法树支持
- SwiftSyntaxMacros:提供实现宏所需要的协议和类型
- SwiftSyntaxBuilder:为展开新代码提供便捷的语法树创建 API
整个工程的源码主要有 4 个部分,截图示意如下
它们分别是
- SwiftMacroKit:包含 Swift 宏的定义,注意这里不提供实现,但是会将定义与实现连接起来
- SwiftMacroKitClient:一个测试工程(所以称为 Client),可以在 main 函数里测试 SwiftMacroKit 定义的宏
- SwiftMacroKitMacros:宏的核心实现部分,最终打包成 macro 产物提供给其他模块使用,例如在 SwiftMacroKit 中引用
- SwiftMacroKitTests:Swift 宏的测试模块,苹果官方推荐我们采用 TDD 的方式开发我们的宏,后面会讲到
创建一个宏
从一个简单的宏开始,以便可以看到它们的工作原理。
因为宏是在编译时运行的,所以我们可以创建一个小宏,返回应用程序构建的日期和时间——这对于调试诊断非常有帮助。
定义宏
首先,需要创建执行宏展开的代码,将 #buildDate 转换为类似于 “2023-06-05T18:00:00Z” 的字符串:
public struct BuildDateMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) -> ExprSyntax {
let date = ISO8601DateFormatter().string(from: .now)
return ""(raw: date)""
}
}
重要提示:这段代码不应该出现在主应用目标中,我们不希望这段代码编译到最终的应用程序中,只需要在其中插入生成的日期字符串。
在同一个模块中,创建一个符CompilerPlugi协议的结构体,导出宏:
import SwiftCompilerPlugin
import SwiftSyntaxMacros
@main
struct MyMacrosPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
BuildDateMacro.self
]
}
然后将其添加到 Package.swift 中的目标列表中:
.macro(
name: "MyMacrosPlugin",
dependencies: [
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
]
)
这样就完成了在外部模块中创建宏的过程。剩下的代码可以放在任何想要使用宏的地方,比如主应用目标中。
这需要两个步骤,首先是定义宏是什么。在例子中,这是一个自由表达式宏,将返回一个字符串,存在于 MyMacrosPlugin 模块中,并且具有严格的名称 BuildDateMacro。因此,我们将这个定义添加到主目标中:
@freestanding(expression)
macro buildDate() -> String =
#externalMacro(module: "MyMacrosPlugin", type: "BuildDateMacro")
实际使用宏
像下面这样:
print(#buildDate)
阅读这段代码时,最重要的是要明白主要的宏功能 —— BuildDateMacro 结构体内的所有代码——在构建时运行,并将其结果注入到调用点。因此,上面的小小 print() 调用将被重写为类似于以下代码:
print("2023-06-05T18:00:00Z")
这也意味着宏内部的代码可以非常复杂,可以以任何想要的方式构建日期,因为实际上完成的代码只看到了返回的字符串。
下面我们尝试一个稍微有用一些的宏,这次是创建一个成员属性宏。当应用于类等类型时,允许将属性应用到类中的每个成员。这在概念上与旧的 @objcMembers 属性相同,将 @objc 添加到类型中的每个属性。
例如,如果有一个可观察对象,它在每个属性上使用 @Published,可以编写一个简单的 @AllPublished 宏来完成这个工作。首先,编写宏本身:
public struct AllPublishedMacro: MemberAttributeMacro {
public static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingAttributesFor member: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [AttributeSyntax] {
[AttributeSyntax(attributeName:SimpleTypeIdentifierSyntax(name: .identifier("Published")))]
}
}
其次,在提供的宏列表中包含下面内容:
struct MyMacrosPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
BuildDateMacro.self,
AllPublishedMacro.self,
]
}
第三,在主应用目标中声明该宏,将其标记为附加的成员属性宏:
@attached(memberAttribute)
macro AllPublished() = #externalMacro(module: "MyMacrosPlugin", type: "AllPublishedMacro")
现在可以将其用于注释可观察对象类:
@AllPublished class User: ObservableObject {
var username = "Taylor"
var age = 26
}
宏可以接受参数来控制其行为,尽管在这里复杂性可能会大幅上升。例如,Swift 团队的 Doug Gregor 维护着一个小的 GitHub 存储库,其中包含一些示例宏,包括一个很强大的宏,在构建时检查硬编码的 URL 是否有效 —— 这样就不可能输入错误的 URL,因为构建过程将停止。
在应用程序目标中声明宏很简单,包括添加一个字符串参数:
@freestanding(expression) public macro URL(_ stringLiteral: String) -> URL = #externalMacro(module: "MyMacrosPlugin", type: "URLMacro")
使用宏也很简单:
let url = #URL("https://swift.org")
print(url.absoluteString)
这将使 url
成为一个完整的 URL 实例,而不是一个可选值,因为在编译时检查了 URL 的正确性。
更难的是宏本身,需要读取传入的字符串 “https://swift.org” 并将其转换为 URL。Doug 的版本更加详细,但如果简化为最基本的形式,会是这样:
public struct URLMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) -> ExprSyntax {
guard let argument = node.argumentList.first?.expression,
let segments = argument.as(StringLiteralExprSyntax.self)?.segments
else {
fatalError("#URL requires a static string literal")
}
guard let _ = URL(string: segments.description) else {
fatalError("Malformed url: (argument)")
}
return "URL(string: (argument))!"
}
}
总结
首先,我们获得的 MacroExpansionContext 值具有一个非常有用的 makeUniqueName() 方法,它将生成一个新的变量名,确保不与当前上下文中的任何其他名称冲突。如果想要将新名称注入到最终的代码中,使用 makeUniqueName() 是一个明智的选择。
其次,宏的一个关注点是在遇到问题时如何调试代码——当无法轻松地逐步执行代码时,很难追踪发生了什么。在 SourceKit 中已经进行了一些工作,将宏扩展为重构操作,但是真正需要看到的是 Xcode 中的实际情况。
最后,宏所能实现的广泛转换可能意味着 Swift Evolution 本身在未来一两年内将发生变化,因为许多以前可能需要大量编译器支持和讨论的功能现在可以使用宏进行原型设计,甚至可能进行发布。