开始写一个 Swift 宏吧 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
iOS 开发实用技术导航
NSHipster 中文版
http://nshipster.cn/
cocos2d 开源 2D 游戏引擎
http://www.cocos2d-iphone.org/
CocoaPods
http://cocoapods.org/
Google Analytics for Mobile 统计解决方案
http://code.google.com/mobile/analytics/
WWDC
https://developer.apple.com/wwdc/
Design Guides and Resources
https://developer.apple.com/design/
Transcripts of WWDC sessions
http://asciiwwdc.com
Cocoa with Love
http://cocoawithlove.com/
Cocoa Dev Central
http://cocoadevcentral.com/
NSHipster
http://nshipster.com/
Style Guides
Google Objective-C Style Guide
NYTimes Objective-C Style Guide
Useful Tools and Services
Charles Web Debugging Proxy
Smore
RayJiang9
V2EX    iDev

开始写一个 Swift 宏吧

  •  
  •   RayJiang9 2023-06-15 11:44:09 +08:00 2217 次点击
    这是一个创建于 848 天前的主题,其中的信息可能已经有所发展或是发生改变。

    image-20230612150131818.png

    什么是宏

    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 等类型使用,可以为其添加代码。

    每个类型的宏具体能干什么?

    @freestanding(expression)

    编写一段代码使其返回一个值。

    let url = #URL("https://www.baidu.com") // 宏内部会判断该字符串能否生成 URL ,如果无法生成会报错,将运行报错提前到了编译阶段。 let url = #URL("https:// www.baidu.com") // 报错:UnableToCreateURL 

    宏生成代码:

    let url = URL(string: "https://www.baidu.com")! 
    @freestanding(declaration)

    宏可以写在任意地方,可以创建一段或多段代码。

    #guardValue(self) 

    宏生成代码:

    guard let self = self else { return } 
    @attached(peer)

    宏会在同个代码层级生成一段代码。

    @AddCompletionHandler() func fetchDetail(_ id: Int) async -> String? { } 

    宏生成代码:

    // 宏会在同个代码层级生成代码 func fetchDetail(_ id: Int, completionHandler: @escaping (String?) -> Void) { Task { completionHandler(await fetchDetail(id)) } } 

    该宏来自 Swift 官方库 声明 实现

    目前在 beta 1 中生成出来的代码无法直接被调用,不清楚是否是宏写的有问题,还是有 Bug 。我更倾向这是 Bug ,上面提到的 #guardValue 宏也无法调用到解包后的变量。如果是我用法的问题,麻烦在评论区告诉我。

    @attached(accessor)

    可以给变量生成 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) } } } 
    @attached(memberAttribute)

    可以给 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() { } } 
    @attached(member)

    可以给 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 } } } 

    宏的实现代码在后面的案例中。

    @attached(conformance)

    可以给 struct/class 添加协议和约束。

    @TestConformance struct Foo { } 

    宏生成代码:

    extension Foo : SomeProtocol where AAA: BBB {} 

    怎么自己创建宏

    写宏的准备工作

    1.创建工程

    新建一个 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 // 宏测试文件,用于编写、调试宏 
    2.宏实现文件

    我们先打开 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, ] } 
    3.宏声明文件

    打开 MyMacro.swift 文件声明一下宏:

    // 如果宏遵守了多个协议,需要在这里写上多个 @attched() @attached(member) public macro CaseDetection() = #externalMacro(module: "MyMacroMacros", type: "CaseDetectionMacro") 
    4.宏测试文件

    打开 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 参数可以获取宏的声明部分,如果宏接收参数可以从 node 中取到,执行 po node

    AttributeSyntax ├─atSignToken: atSign ─attributeName: SimpleTypeIdentifierSyntax ─name: identifier("CaseDetection") 

    如果我们想要获取宏的名称可以这样写:

    let macrOname= node.attributeName.description // "CaseDetection" 
    declaration

    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 站不支持折叠代码,示例代码比较长就删掉了,可以去掘金看代码。

    2 条回复    2023-06-15 22:13:56 +08:00
    maxmak
        1
    maxmak  
       2023-06-15 13:39:42 +08:00
    其实是知道,看你这个搞到都不知道怎么用了
    xingheng
        2
    xingheng  
       2023-06-15 22:13:56 +08:00 via iPhone
    感觉更像是 decoration ,或者高阶一点儿的 code snippet ,不像是 C 里的 macro
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     884 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 31ms UTC 21:19 PVG 05:19 LAX 14:19 JFK 17:19
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86