iOS-Swift 宏
什么是 swift macro
Swift 宏在 WWDC 2023 的 Swift 5.9 版本中引入,简单来说它允许我们在编译时生成重复代码,它还允许我们在编译之前动态地操作项目的 Swift 代码,从而允许我们在编译时注入额外的功能,使我们的应用程序的代码库更易于阅读且更高效地编码。
OC 时代的宏就是简单的代码替换,相对来说比较简单,而 Swift 宏设计的比较抽象,很难理解,但功能也更加强大,更复杂,我研究了一段时间,来尝试给大家分享一下。
swift 宏有一条原则是只能添加代码,不会删除或修改现有代码。
宏的分类
按照苹果官方文档,有两种类型的宏:
1、独立宏,声明的时候使用 @freestanding
关键字,使用的时候以标签 (#) 开头,并在后边的括号里添加相应的参数,主要作用是代替代码中的内容,有点像 OC 的宏,比如官方示例中的 #stringify(a + b)
,或者 #warning("这是一个警告")
。
2、附加宏,声明的时候使用 @attached
关键字,使用的时候以 @ 开头,并在后边的括号里添加相应的参数,主要作用是为声明添加代码,比如 @OptionSet<Int>
。
写一个宏
1、创建宏模块
让我们跟着官方示例写一个宏,每个 Swift Macro 都是一个 Package,首先新建 Swift Macro Package,Xcode 顶部菜单 File -> New -> Package
,选择 Swift Macro,下一步之后填入名称(我这里填写的名称是 MyFirstMacro
),点击确认即可。
或者使用快捷键 shift + control + command + N
创建好之后可以看到,Xcode 自动帮我们生成了一个 Macro 的 demo,包含 4 个主要的代码文件,宏的声明、宏的实现、宏的测试 和 宏的使用。
2、宏的声明
我们来分别解读一下这四个文件,首先是声明部分:
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "MyFirstMacroMacros", type: "StringifyMacro")
在宏的分类部分我们讲到了 @freestanding
关键字用来声明独立宏,因此这个 demo 是个独立宏。
那 freestanding
后边括号里的 expression
又是什么呢?
官方把它称为 Macro roles
,也就是宏角色,不同的宏角色意味着可以实现不同功能的宏,在 WWDC 的 Session 中,Apple 对所有的角色做了说明,我这里也贴一下这张表:
角色名 | 作用 |
---|---|
@freestanding(expression) | 创建一段返回值的代码 |
@freestanding(declaration) | 创建一个或多个声明。如 struct、function、variable 或 type |
@attached(peer) | 在其所应用的声明旁边添加新声明 |
@attached(accessor) | 向属性添加访问器。例如为 var 属性添加 get 和 set 方法。 |
@attached (memberAttribute) | 将属性添加到其所应用的类型/扩展中的声明中 |
@attached (member) | 在其所应用的类型/扩展内添加新声明。例如。在 struct 内添加自定义 init() |
@attached(conformance) | 添加对协议的一致性,比如为作用的类型自动实现某个协议 |
同一个宏支持添加多个角色组合,比如:
@attached(accessor)
@attached(memberAttribute)
@attached(member, names: named(init()))
public macro ...
注意这个声明必须要声明 public,因为声明宏的代码与使用该宏的代码位于不同的模块。
等号的后面是 #externalMacro(module: "MyFirstMacroMacros", type: "StringifyMacro"),这是另一个独立宏,用来设定这个宏的对应的模块名是 MyFirstMacroMacros
,对应的 type 是 StringifyMacro
。
2、宏的实现
我们打开 MyFirstMacroMacro.swift
文件,对应上边 #externalMacro
指定的模块名,这里就是宏的实现部分了。
public struct StringifyMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) -> ExprSyntax {
guard let argument = node.argumentList.first?.expression else {
fatalError("compiler bug: the macro does not have any arguments")
}
return "(\(argument), \(literal: argument.description))"
}
}
首先这里有个结构体 StringifyMacro
,对应的是上边 #externalMacro
指定的 type。
StringifyMacro
遵守了 ExpressionMacro
协议,这个协议是必须遵守的,为了实现不同的宏,每种角色都有一个必须遵守的协议,下边是对照表:
角色名 | 对应协议 |
---|---|
@freestanding(expression) | ExpressionMacro |
@freestanding(declaration) | DeclarationMacro |
@attached(peer) | PeerMacro |
@attached(accessor) | AccessorMacro |
@attached (memberAttribute) | MamberAttributeMacro |
@attached (member) | MemberMacro |
@attached(conformance) | ConformanceMacro |
我们继续分析这个 Demo 的实现,它实现了 ExpressionMacro
协议的方法:
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) -> ExprSyntax
并在其内部 return "(\(argument), \(literal: argument.description))"
。要想在这个函数内做更多的事情,需要先学习 swift-syntax
这个库,其实只要创建宏模块,就会自动帮我们依赖这个库,我们的 Demo 也是如此:
因为这里所有语法和用到的类都是基于这个库的,我还没有好好研究,所以这里先不展开讲了,等我学完了再来分享。
简单说一下,这里其实就是返回了一个元组,元组里有两个值,第一个是 #stringify
传入的参数的值,第二个是 #stringify
传入的参数的描述。
此文件最下方还有个结构体:
@main
struct MyFirstMacroPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
StringifyMacro.self,
]
}
这段代码的作用是将我们的宏导出,如果没有这段代码也将无法正常使用。
3、宏的使用
打开 main.swift
文件,上边提到宏的声明和使用在不同模块,因此要想使用我们的宏,需要先导入
import MyFirstMacro
然后调用 #stringify(你的参数)
,Demo 里声明了两个 Int,a 和 b,调用 #stringify(a + b)
let a = 17
let b = 25
let (result, code) = #stringify(a + b)
print("The value \(result) was produced by the code \"\(code)\"")
// 打印结果:
// The value 42 was produced by the code "a + b"
可以从上边代码中看出,#stringify
返回一个元组,第一个值 result 为 a + b 的值,为 42,第二个值为参数的描述:"a + b"。
如果其他人接手你的代码,可能导致他不知道 stringify 究竟生成了什么代码,导致维护困难,Xcode 提供了一种方式可以直接将宏展开,展开之后即可看到当前这个宏生成了什么代码,选中宏之后,鼠标右键,在菜单中选择 Expand Macro。
将宏展开之后将在这一行的下方展示展开后的代码:
4、宏的测试
我们来到 MyFirstMacroTests.swift
文件,这里是宏的单元测试:
let testMacros: [String: Macro.Type] = [
"stringify": StringifyMacro.self,
]
final class MyFirstMacroTests: XCTestCase {
func testMacro() {
assertMacroExpansion(
"""
#stringify(a + b)
""",
expandedSource: """
(a + b, "a + b")
""",
macros: testMacros
)
}
}
上边的代码调用 assertMacroExpansion
方法传入了两个字符串,第一个是宏的调用 #stringify(a + b)
,第二个是展开后的结果 (a + b, "a + b")
,第三个参数 macros 是我们要测试的宏。
点击函数名左侧的菱形开始测试
之后 Xcode 会运行宏的实现部分,会拿实际返回结果和期待的展开结果做对比,如果 #stringify(a + b)
展开的结果刚好是 (a + b, "a + b")
,则用例通过,否则用例失败。
5、宏的调试
要开发一个宏,避免不了需要进行调试。
调试和正常代码调试方式差不多,可以在宏的实现的部分打上断点,或者用 print 来打印,然后运行上一步的单元测试,注意一定要运行单元测试,断点和 print 才能执行
我们会看到,print 打印的参数是这种格式:
[SequenceExprSyntax
╰─elements: ExprListSyntax
├─[0]: IdentifierExprSyntax
│ ╰─identifier: identifier("a")
├─[1]: BinaryOperatorExprSyntax
│ ╰─operatorToken: binaryOperator("+")
╰─[2]: IdentifierExprSyntax
╰─identifier: identifier("b")]
这其实是抽象语法树(AST),也是 SwiftSyntax 的部分,关于 AST 详细的介绍之后单独出文章给大家介绍。
如果你想继续熟悉这种语法树,可以到这个在线网站[1]使用。
最后
swift macro
是个非常强大的新功能,预计将来在项目里也会被大量使用,今天我们主要介绍的是一个很简单的示例,其实在实际的应用场景中,要远比这强大,已经有很多大佬在为开源宏努力。
正如你所看到的,宏非常抽象,难以理解。如果能学会编写宏,对个人技术成长来说绝对是件好事。
大家如果第一遍不能看懂,可以先把这篇文章收藏,然后慢慢开始实践,再回头看这篇文章,相信你会有新的收获。