一、Swift Macro介绍
WWDC2023会上Swift 5.9加入了Swift Macro,它允许我们在编译时生成代码或在编译之前动态地操作项目的 Swift 代码,从而实现在编译时注入额外的功能,使我们的应用程序的代码库更易于阅读且更高效地编码。
Swift Macro没有iOS版本要求,只要使用Swift 5.9及以上都可以。
OC Macro就是简单的代码替换,而 Swift 宏设计的比较抽象,可以通过框架提供的API直接获取到Swift的句法树,获取或添加需要的代码,功能更加强大。
Swift Macro源码地址 注:本文基于510.0.3版本
本文主要目的是记录以下Swift Macro的用法和每种类型的宏的作用,方便快速入门。如果内容有误或理解有误,希望各位大佬指正,谢谢。
二、初探Swift Macro
1.创建Swift Macro工程
在Xcode的File菜单中选择NEW->Package->Swift Macro,如图:
先看看默认生成的Macro程序,它会默认实现一个
stringify
Macro。2.文件结构
这里只先简单介绍一下,具体代码实现及作用后文再详细分析。
2.1.SwiftMacroMacro.swift 宏实现
struct StringifyMacro
就是stringify macro实现的源码。
structSwiftMacroPlugin
类似于注册宏,实现的宏都要添加到providingMacros
数组里才会生效,否则使用时会报错。
2.2.SwiftMacro.swift 宏声明
通过macro
关键词定义一个stringify宏,具体结构和声明方法类似。
等号右边赋值部分又是另一个宏,这个宏专门用来定义其他宏的,传入需定义宏所在模块和类型(也就是上面的structStringifyMacro
)。
@freestanding(expression)
官方定义为MacroRole
宏角色(如下图MacroRole的定义)。相当于定义这个宏具备什么功能,不同的关键词决定了这个宏具备不同的功能,并且是可以添加多个这种关键词的,具体每个角色等后文讲解各类型宏作用时就自然理解了。
2.3.main.swift 宏运用
这个文件就是宏在项目中的实际运用,可以看到传入了a + b
,返回了他们的相加结果a + b = 42
,并且还能把"a + b"作为字符串返回。怎么做到的呢?等看完下文分析ExpressionMacro
时,自然就理解了。
我们还可以右键宏,在菜单里点击Expand Macro展开宏,也就是预览这个宏编译后会转换成什么代码,依次来确认宏是否正确的生成我们想要的代码。如果发现点击后没任何效果,就要排查宏是不是那里写错了。
2.3.SwiftMacroTests.swift 宏测试
除了上面直接使用宏来做测试以外,我们还可以用苹果为我们提供的测试工具来进行测试。这个测试类运行后是可以在宏实现里面断点调试的,2.2宏运用运行起来断点是不起作用的。
等后面我们使用例子分析代码时,就会使用到这个测试类来断点查看变量,追踪问题。
三、Swift Macro分类
从上图可以看到官方为我们提供了很多种宏,但实际目前分为两类宏。
1.FreestandingMacro
:即独立宏,宏声明中使用 @freestanding
关键字,宏运用时以#
开头。独立宏和OC的宏类似,就是将宏替换宏中预定义的代码。 上一个章节中的#stringify(a + b)就是一个独立宏。
2.AttachedMacro
:附加宏,宏声明中使用@attached
关键字,宏运用时以@
开头。作用是为类、结构体、协议、属性、方法等目标声明
附加额外的代码(只能增加,不能修改删除原有代码)。
iOS17为我们提供的新的监听框架import Observation
中的@Observable
就是一个附加宏。
swift macro其实基础入门并不难,比较麻烦的是需要处理各种语法、重复添加代码等特殊情况,想要自己的宏更加完善,就需要兼容这些特殊情况。比如:
// 1.定义变量有多种语法,需要分别处理
var a: Int = 0 // 标准写法,可以拿到变量类型
var a = 0 // 不能从语法树中获取变量类型,需要自己判断
var a = 0, b = 0 // 有多个变量,需要遍历获取
// 2.给成员变量添加didSet访问器,如果外部代码已经手动添加了didSet。
// 宏内就要自己做处理,根据自己的需求选择抛异常或者返回空。
// 3.定义了一个给类使用的宏,不添加检查代码的话,外部是可以错误的给协议、结构体等添加的。
四、自定义宏
开始之前,有一些名词需要先解释一下:
-
declaration
声明:指宏添加到的目标对象,可以是类、结构体、属性、方法等。 -
member
成员:即属性、方法。 -
attribute
属性: 指声明前面添加的修饰符,Swift Attributes。比如@Observable
、@available(*, deprecated)
等,这些都算是属性。
1.独立宏
1.1.ExpressionMacro
在第二章节创建的宏工程默认实现的宏StringifyMacro
就是一个ExpressionMacro
表达式宏。
public struct StringifyMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) -> ExprSyntax {
guard let argument = node.arguments.first?.expression else {
fatalError("compiler bug: the macro does not have any arguments")
}
return "(\(argument), \(literal: argument.description))"
}
}
在宏声明中需要对表达式宏添加@freestanding(expression)
来表示这个宏是一个表达式类型的独立宏。
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "SwiftMacroMacros", type: "StringifyMacro")
下图就是此宏生成的Abstract Syntax Tree 抽象语法树。可以很清晰的看到我们使用宏是传入的表达式,多个操作符operator就会生成更深层级的嵌套。
此宏主要用于表达式的计算或转换。需要传入一个表达式,根据自身需求可对其中各节点进行修改。
比如传入的操作符+
可以改成-
,再将修改后的ExprSyntax
作为返回参数传递出去,就能将原本的加法改为减法。
以下是宏#stringify(a + b + c)的语法树结构:
1.2.DeclarationMacro
可以用来定义类、结构体、枚举、方法等,起到声明的作用。如下代码获取外部传入参数.stringSegment(name) = expression.segments.first
解析出参数字段name
。然后生成了2个结构体struct (name)
和struct (name)Sub
public struct DeclarationTestMacro: DeclarationMacro {
public static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> [DeclSyntax] {
guard let expression = node.arguments.first?.expression.as(StringLiteralExprSyntax.self),
expression.segments.count == 1,
case let .stringSegment(name) = expression.segments.first else {
return []
}
let syntax1 = DeclSyntax(stringLiteral:
"""
struct \(name) {
var a = 0
var b = 0
}
""")
let syntax2 = DeclSyntax(stringLiteral:
"""
struct \(name)Sub {
var a = 0
var b = 0
}
""")
return [syntax1, syntax2]
}
}
宏声明如下:
因为这个宏定义了一个结构体,这里需要标记names,否则会报错:
这个参数有4种定义:
1.arbitrary: 即任意名字都行
2.named(name): 必须是指定的名字
3.prefixed(name): 前缀必须是指定字符串(实际测试中疑似对declaration宏不生效,待确认)。
4.suffixed(name): 后缀必须是指定字符串(实际测试中疑似对declaration宏不生效,待确认)。
@freestanding(declaration, names: arbitrary)
public macro declarationStruct(_ value: String) = #externalMacro(module: "SwiftMacroMacros", type: "DeclarationTestMacro")
使用时展开宏即可预览替换后的结果:
1.3.CodeItemMacro
此宏还是一个未启用的实验性功能,不能正式使用。但是可以在SwiftMacroTests
测试类中调试。(由于是实验性质的功能,这里就只粗略展示一下用法)
在第二章节创建的宏工程的SwiftMacroMacro
类实现中添加struct CodeItemTestMacro
,实现CodeItemMacro
协议。
CodeItemTestMacro宏只是简单的实现了一个判断传入的参数是否等于0
比如将#codeItemTest(a, b)转换为
if a == 0 {
return
}
if b == 0 {
return
}
具体的宏实现代码如下(下文有讲解具体代码含义):
public struct CodeItemTestMacro: CodeItemMacro {
public static func expansion(of node: some SwiftSyntax.FreestandingMacroExpansionSyntax, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.CodeBlockItemSyntax] {
let identifiers = try node.arguments.map { argument in
guard let declReferenceExpr = argument.expression.as(DeclReferenceExprSyntax.self) else {
throw MacroError.codeItem
}
return declReferenceExpr.baseName
}
let items = identifiers.compactMap { syntax -> CodeBlockItemSyntax in
"""
if \(raw: syntax.text) == 0 {
return
}
"""
}
return items
}
}
enum MacroError: Error {
case codeItem
}
测试用例中测试代码如下:
func testCodeItemMacro() throws {
#if canImport(SwiftMacroMacros)
assertMacroExpansion(
"""
#codeItemTest(a, b)
""",
expandedSource: """
if a == 0 {
return
}
if b == 0 {
return
}
""",
macros: testMacros
)
#else
throw XCTSkip("macros are only supported when running tests for the host platform")
#endif
}
//测试结果,可以看到测试通过:
Test Suite 'Selected tests' started at 2024-08-21 11:18:01.012.
Test Suite 'SwiftMacroTests.xctest' started at 2024-08-21 11:18:01.012.
Test Suite 'SwiftMacroTests' started at 2024-08-21 11:18:01.012.
Test Case '-[SwiftMacroTests.SwiftMacroTests testCodeItemMacro]' started.
Test Case '-[SwiftMacroTests.SwiftMacroTests testCodeItemMacro]' passed (0.008 seconds).
Test Suite 'SwiftMacroTests' passed at 2024-08-21 11:18:01.020.
Executed 1 test, with 0 failures (0 unexpected) in 0.008 (0.008) seconds
Test Suite 'SwiftMacroTests.xctest' passed at 2024-08-21 11:18:01.021.
Executed 1 test, with 0 failures (0 unexpected) in 0.008 (0.009) seconds
Test Suite 'Selected tests' passed at 2024-08-21 11:18:01.021.
Executed 1 test, with 0 failures (0 unexpected) in 0.008 (0.009) seconds
Program ended with exit code: 0
打断点追踪协议方法都提供了哪些属性可以让我们获取。从下图可以看到node有个arguments数组,里面包含了传入的参数信息,所以这个宏实现我们就是去获取baseName,然后将baseName转换为上面的if-else代码。
2.附加宏
2.1.AccessorMacro
这个宏是给成员变量附加访问器的(比如didSet、willSet、getter、setter)。需要注意的是,如果本身已经实现了didSet,宏又去添加一个didSet,不做处理的话会重复添加导致报错,一般需要在宏实现中自己做判断,下面的例子只是简单的展示一下用法,不会做过多的容错处理。
public struct AccessorTestMacro: AccessorMacro {
public static func expansion(of node: SwiftSyntax.AttributeSyntax,
providingAccessorsOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext) throws -> [AccessorDeclSyntax] {
guard let property = declaration.as(VariableDeclSyntax.self) else {
return []
}
guard let pattern = property.bindings.first?.pattern.as(IdentifierPatternSyntax.self) else {
return []
}
let identifier = pattern.identifier.trimmed
if case .argumentList(let list) = property.attributes.first?.as(AttributeSyntax.self)?.arguments,
let first = list.first,
let statements = first.expression.as(ClosureExprSyntax.self)?.statements {
let didSet: AccessorDeclSyntax =
"""
didSet {
print("宏内部添加的代码")
\(statements.trimmed)
}
"""
return [didSet]
} else {
let didSet: AccessorDeclSyntax =
"""
didSet {
print(\(identifier))
}
"""
return [didSet]
}
}
}
宏声明如下:
这里加了一个入参block,是为了演示外部可以通过一定的方法将要执行的代码块添加到宏生成的访问器里(这种实现有一定的局限性,比如不能随意访问self及其他成员变量),这里只做演示用。
@attached(accessor, names: named(didSet))
由于AccessorMacro是一个附加宏,所以不能像上一节的独立宏那样使用freestanding
来修饰了。然后指定只生成didSet访问器。
/// 实验传代码块
@attached(accessor, names: named(didSet))
public macro accessorTest<T>(_ block:((T)-> Void)? = nil) = #externalMacro(module: "SwiftMacroMacros", type: "AccessorTestMacro")
/// 常规用法
@attached(accessor, names: named(didSet))
public macro accessorTest() = #externalMacro(module: "SwiftMacroMacros", type: "AccessorTestMacro")
在下面的使用代码中右键菜单展开宏可以预览到最终代码会是什么样的。
生成didSet访问器很简单,可以直接用字符串定义。然后是从下图中打印的语法树可以找到传入的block,然后一步步提取其中的代码块,然后插入到didSet中(再次提醒此例子代码比较简单,许多容错都没有处理)。
实际上@accessorTest<Int>({ a in print()}) 这里也是取巧并非真的从宏里面将a
变量传递出来了,只是借用同名a
让代码块附加到didSet后能找到a
这个变量。
2.2.ExtensionMacro
此宏用于创建Extension,下面的代码创建了一个Extension并遵守Equatable,很简单。
public struct ExtensionTestMacro: ExtensionMacro {
public static func expansion(of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
let syntax: ExtensionDeclSyntax? =
try? .init(.init(stringLiteral:
"""
extension \(type.trimmed): Equatable {}
"""
))
guard let syntax else {
return []
}
return [syntax]
}
}
@attached(extension, conformances: Equatable)
public macro extensionTest() = #externalMacro(module: "SwiftMacroMacros", type: "ExtensionTestMacro")
2.3.MemberAttributeMacro
为声明中的成员添加属性。声明、成员、属性指什么,可以看文章第四节开头。
这里我们就可以用此宏给声明的成员附加我们上面2.1实现的accessorTest
宏。不做判断处理的话,默认是会给所有成员附加属性,我们也可以根据自己需要添加逻辑过滤不想附加属性的成员。
public struct MemberAttributeTestMacro: MemberAttributeMacro {
public static func expansion(of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingAttributesFor member: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [AttributeSyntax] {
return [
AttributeSyntax(attributeName: IdentifierTypeSyntax(name: .identifier("accessorTest"))),
]
}
}
@attached(memberAttribute)
public macro memberAttributeTest() = #externalMacro(module: "SwiftMacroMacros", type: "MemberAttributeTestMacro")
2.4.MemberMacro
为声明添加额外的成员。
如下例子中:给声明(即实现中为结构体)添加了一个flag
属性(成员)和一个memberAttributePrint
方法(成员)。该方法中打印了声明的属性和flag
的值。
public struct MemberTestMacro: MemberMacro {
public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
let syntax1: DeclSyntax =
"""
var flag: Bool = false
func memberAttributePrint() {
print("\(node.attributeName.trimmed)-\\(flag)")
}
"""
return [syntax1]
}
}
@attached(member, names: arbitrary)
public macro memberTest() = #externalMacro(module: "SwiftMacroMacros", type: "MemberTestMacro")
2.5.PeerMacro
根据成员附加一个新成员。
下面的例子中,我们根据属性生成了一个新的属性var (a)Peer: Int = 0
,新的属性的类型、初始化与目标属性保持一致,并且还附带了@accessorTest
宏,为新属性添加didSet访问器。
public struct PeerTestMacro: PeerMacro {
public static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard let property = declaration.as(VariableDeclSyntax.self) else {
return []
}
guard let identifier = property.bindings.first?.pattern.as(IdentifierPatternSyntax.self)?.trimmed,
let type = property.bindings.first?.typeAnnotation?.type.trimmed,
let initializer = property.bindings.first?.initializer else {
return []
}
let syntax: DeclSyntax =
"""
@accessorTest
var \(identifier)Peer: \(type) \(raw: initializer)
"""
return [syntax]
}
}
@attached(peer, names: arbitrary)
public macro peerTest() = #externalMacro(module: "SwiftMacroMacros", type: "PeerTestMacro")
下面是宏展开的效果:
这里出Bug了,@accessorTest宏无法展开,但通过运行可以看出附加的访问器是生效了的,打印了属性的值5
2.6.聚合使用
我们可以一个宏实现里实现多个宏协议,然后声明宏时使用多个@attached
修饰。
@attached(accessor, names: named(didSet))
@attached(peer, names: arbitrary)
public macro ObservableTracked() = #externalMacro(module: "ObservationMacros", type: "ObservableTrackedMacro")