Apple 在 Swift 5.9 里面加入了 Swift macros (宏),宏可以在编译的过程中帮我们生成一些需要重复编写的代码。WWDC 23 中有两个关于宏的 Session ,Expand on Swift macros 介绍了什么是宏和宏的几种类型,Write Swift macros 介绍了怎么去写一个宏。这两个 Session 介绍了每种宏可以做什么,但是缺少了详细的代码,我不知道具体要怎么去实现我想要的效果,在查阅了一些资料和 Swift 官方库的内部实现之后才知道每个宏的定义和用法。
宏主要分为两种类型:
@freestanding
是一个独立的宏(与 #
语法一起使用),并且可以用作表达式。
@attached
是一个附加宏(与 @
语法一起使用),需要搭配 struct/class/enum/property/function 等类型使用,可以为其添加代码。
编写一段代码使其返回一个值。
let url = #URL("https://www.baidu.com") // 宏内部会判断该字符串能否生成 URL ,如果无法生成会报错,将运行报错提前到了编译阶段。 let url = #URL("https:// www.baidu.com") // 报错:UnableToCreateURL
宏生成代码:
let url = URL(string: "https://www.baidu.com")!
宏可以写在任意地方,可以创建一段或多段代码。
#guardValue(self)
宏生成代码:
guard let self = self else { return }
宏会在同个代码层级生成一段代码。
@AddCompletionHandler() func fetchDetail(_ id: Int) async -> String? { }
宏生成代码:
// 宏会在同个代码层级生成代码 func fetchDetail(_ id: Int, completionHandler: @escaping (String?) -> Void) { Task { completionHandler(await fetchDetail(id)) } }
目前在 beta 1 中生成出来的代码无法直接被调用,不清楚是否是宏写的有问题,还是有 Bug 。我更倾向这是 Bug ,上面提到的
#guardValue
宏也无法调用到解包后的变量。如果是我用法的问题,麻烦在评论区告诉我。
可以给变量生成 get 、set 、willSet 、didSet 等方法。
class Foo { @PrintWhenAssigned var name: String = "" } let f = Foo() f.name = "Tom" // Logs: Tom f.name = "Bob" // Logs: Bob
宏生成代码:
class Foo { @PrintWhenAssigned var name: String = "" { didSet { print(name) } } }
可以给 struct/class/enum 等里面的属性、方法加上 attribute ,比如 @property 、宏 等。
@TestMemberAttribute public class Foo { var name: String = "" func foo() { } }
宏生成代码:
@TestMemberAttribute public class Foo { @SomeMacro var name: String = "" @SomeMacro func foo() { } }
可以给 struct/class/enum 添加属性、方法。
@CaseDetection enum Animal { case cat(String) }
宏生成代码:
@CaseDetection enum Animal { case cat(String) var isCat: Bool { if case .cat = self { true } else { false } } }
宏的实现代码在后面的案例中。
可以给 struct/class 添加协议和约束。
@TestConformance struct Foo { }
宏生成代码:
extension Foo : SomeProtocol where AAA: BBB {}
新建一个 Swift Macro Package
,Xcode -> File -> New -> Package ,选择 Swift Macro
。
Swift Macro 需要依赖 apple/swift-syntax 第三方库,这是 Apple 的词法分析库,用于解析、检查、生成和转换 Swift 源代码。
创建完成后,我们可以看到项目的结构是这样的:
├── Package.resolved ├── Package.swift ├── Sources │ ├── MyMacro │ │ └── MyMacro.swift // 宏声明文件 │ ├── MyMacroClient │ │ └── main.swift // 可运行文件,可以在这里测试宏的实际效果 │ └── MyMacroMacros │ └── MyMacroMacro.swift // 宏实现文件 └── Tests └── MyMacroTests └── MyMacroTests.swift // 宏测试文件,用于编写、调试宏
我们先打开 MyMacroMacro.swift
写一下上面提到的 @CaseDetection
宏。先让宏遵守 MemberMacro
协议,然后点击报错让 Xcode 生成协议方法,生成之后先返回一个空数据,并将断点打到 return []
上面,不着急写宏。
public struct CaseDetectionMacro { } extension CaseDetectionMacro: MemberMacro { public static func expansion<Declaration: DeclGroupSyntax, Context: MacroExpansionContext>( of node: AttributeSyntax, providingMembersOf declaration: Declaration, in context: Context ) throws -> [DeclSyntax] { return [] } }
然后我们需要在底部将宏加到 MyMacroPlugin
里面。
@main struct MyMacroPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ StringifyMacro.self, CaseDetectionMacro.self, ] }
打开 MyMacro.swift
文件声明一下宏:
// 如果宏遵守了多个协议,需要在这里写上多个 @attched() @attached(member) public macro CaseDetection() = #externalMacro(module: "MyMacroMacros", type: "CaseDetectionMacro")
打开 MyMacroTests.swift
文件写一个测试用例,目的是为了能断点打到宏里面。
先在 testMacros
里面加上我们的宏:
let testMacros: [String: Macro.Type] = [ "stringify": StringifyMacro.self, "CaseDetection": CaseDetectionMacro.self, ]
再写一个测试用例,这里 expandedSource
是宏预期生成出来的代码,我们可以先不写。
func testCaseDetectionMacro() { assertMacroExpansion( """ @CaseDetection enum Animal { case cat } """, expandedSource: """ """, macros: testMacros ) }
运行测试用例,我们就会进入宏实现的断点里面了,这时候我们可以开始写宏了。
public static func expansion<Declaration: DeclGroupSyntax, Context: MacroExpansionContext>( of node: AttributeSyntax, providingMembersOf declaration: Declaration, in context: Context ) throws -> [DeclSyntax] { return [] }
node
参数可以获取宏的声明部分,如果宏接收参数可以从 node
中取到,执行 po node
。
AttributeSyntax ├─atSignToken: atSign ─attributeName: SimpleTypeIdentifierSyntax ─name: identifier("CaseDetection")
如果我们想要获取宏的名称可以这样写:
let macrOname= node.attributeName.description // "CaseDetection"
declaration
参数可以获取类型里面的定义,执行 po declaration
:
EnumDeclSyntax ├─attributes: AttributeListSyntax │ ─[0]: AttributeSyntax │ ├─atSignToken: atSign │ ─attributeName: SimpleTypeIdentifierSyntax │ ─name: identifier("CaseDetection") ├─enumKeyword: keyword(SwiftSyntax.Keyword.enum) ├─identifier: identifier("Animal") ─memberBlock: MemberDeclBlockSyntax ├─leftBrace: leftBrace ├─members: MemberDeclListSyntax │ ─[0]: MemberDeclListItemSyntax │ ─decl: EnumCaseDeclSyntax │ ├─caseKeyword: keyword(SwiftSyntax.Keyword.case) │ ─elements: EnumCaseElementListSyntax │ ─[0]: EnumCaseElementSyntax │ ─identifier: identifier("cat") ─rightBrace: rightBrace
宏需要获取枚举的名称,我们现在断点里面获取到想要的数据,再去写代码。
我们一步步去点开,会发现到 decl
就下不去了。
po declaration.memberBlock.members.first!.decl
因为 decl
是顶层的协议 DeclSyntax
,我们需要使用 as()
将其转换为 EnumCaseDeclSyntax
:
po declaration.memberBlock.members.first!.decl.as(EnumCaseDeclSyntax.self)
在写宏的过程中,我们会经常遇到这个问题,发现类型对不上可以用 as()
进行类型转换,最终的调试代码:
po declaration.memberBlock.members.first!.decl.as(EnumCaseDeclSyntax.self)?.elements.first!.identifier.description // "cat"
根据这个调试代码,我们可以去写宏实现代码了。
public struct CaseDetectionMacro { } extension CaseDetectionMacro: MemberMacro { public static func expansion<Declaration: DeclGroupSyntax, Context: MacroExpansionContext>( of node: AttributeSyntax, providingMembersOf declaration: Declaration, in context: Context ) throws -> [DeclSyntax] { var names: [String] = [] for member in declaration.memberBlock.members { // 循环获取所有属性、方法 let elements = member.decl.as(EnumCaseDeclSyntax.self)?.elements if let propertyName = elements?.first?.identifier.description { names.append(propertyName) // 取出枚举名 } } return names.map { // 拼接实现代码 """ var \("is" + capitalized($0)): Bool { if case .\($0) = self { true } else { false } } """ }.map { DeclSyntax(stringLiteral: $0) } } /// 首字母大写 private static func capitalized(_ str: String) -> String { var str = str let firstChar = String(str.prefix(1)).uppercased() str.replaceSubrange(...str.startIndex, with: firstChar) return str } }
最后我们到 main.swift
里面写一个枚举测试一下宏。
@CaseDetection enum Animal { case cat }
写完我们可以右击 code>@CaseDetection 宏,点击 Expand Macro
查看宏生成的代码。
Declaration name 'isCat' is not covered by macro 'CaseDetection'
宏生成的代码非常完美,但是编辑报错了,这是因为宏生成出来的变量 /方法需要在宏声明部分定义好,回到 MyMacro.swift
宏声明文件修改一下声明代码:
@attached(member, names: arbitrary) public macro CaseDetection() = #externalMacro(module: "MyMacroMacros", type: "CaseDetectionMacro")
注意:arbitrary
表示宏可以生成任意变量 /方法,在这个例子中,由于我们要生成的变量是动态变化的,所以只能写 arbitrary
,如果你的宏生成的变量 /方法是固定的,建议在这里也固定写死,比如:
@attached(member, names: named(isCat)) public macro CaseDetection() = #externalMacro(module: "MyMacroMacros", type: "CaseDetectionMacro")
我们再运行就发现编译通过了,最后的最后,记得去完善测试用例~
宏非常强大,可以帮我们省去很多重复的代码,虽然写宏的过程会比较麻烦,但是写完之后就可以为你节省非常多的时间。另外每一个类型的宏都是 protocol
,所以我们可以将多个宏组合在一起使用,比如 Swift Data
里面的 @Model
。目前宏还在 beta 测试阶段,后续 Apple 也可能会对宏进行改进,我也会持续关注并更新哒。
由于 V 站不支持折叠代码,示例代码比较长就删掉了,可以去掘金看代码。
1 maxmak 2023-06-15 13:39:42 +08:00 其实是知道,看你这个搞到都不知道怎么用了 |
![]() | 2 xingheng 2023-06-15 22:13:56 +08:00 via iPhone 感觉更像是 decoration ,或者高阶一点儿的 code snippet ,不像是 C 里的 macro |