当前位置: 首页 > article >正文

Swift 宏(Macro)入门趣谈(五)

在这里插入图片描述

概述

苹果在去年 WWDC 23 中就为 Swift 语言新增了“其利断金”的重要小伙伴 Swift 宏(Swift Macro)。为此,苹果特地用 2 段视频(入门和进阶)颇为隆重的介绍了它。

在这里插入图片描述

那么到底 Swift 宏是什么?有什么用?它和 C/C++ 语言中的宏又有什么异同呢?本系列博文将会尝试为小伙伴们揭开 Swift 宏的神秘面纱。

在本篇博文中,您将学到如下内容:

  • 概述
    • 5.4 完成宏主体
    • 5.5 验证宏展开后的结果
  • 6. 另一个简洁的解决方案
  • 7. 目前 Swift 宏的不足之处
  • 总结

相信学完本系列博文后,Swift Macro 会从大家心中的“阳春白雪”变为“阳阿薤露”,小伙伴们必可以将它们运用的“如臂使指”。

那还等什么呢?Let‘s go!!!😉


5.4 完成宏主体

在完成了 @nilable 宏接口、客户端代码以及宏主体初步结构的搭建之后,现在回到 NilableMacro 结构的定义中,我们现在可以尝试补全 expansion() 方法中所有展开代码了。

不过在撸码之前,我们有必要先来了解一下 expansion() 方法传入实参所扮演的角色。在这个例子中,我们只关心其中的 declaration 参数,它是一个 some DeclSyntaxProtocol 类型:

providingPeersOf declaration: some DeclSyntaxProtocol

在 Xcode 调试控制台中,我们利用 po 命令可以列出它的结构细节:

FunctionDeclSyntax
├─attributes: AttributeListSyntax
│ ╰─[0]: AttributeSyntax
│   ├─atSign: atSign
│   ╰─attributeName: IdentifierTypeSyntax
│     ╰─name: identifier("nilable")
├─modifiers: DeclModifierListSyntax
│ ├─[0]: DeclModifierSyntax
│ │ ╰─name: keyword(SwiftSyntax.Keyword.static)
│ ╰─[1]: DeclModifierSyntax
│   ╰─name: keyword(SwiftSyntax.Keyword.private)
├─funcKeyword: keyword(SwiftSyntax.Keyword.func)
├─name: identifier("test")
├─genericParameterClause: GenericParameterClauseSyntax
│ ├─leftAngle: leftAngle
│ ├─parameters: GenericParameterListSyntax
│ │ ├─[0]: GenericParameterSyntax
│ │ │ ├─attributes: AttributeListSyntax
│ │ │ ├─name: identifier("Root")
│ │ │ ╰─trailingComma: comma
│ │ ╰─[1]: GenericParameterSyntax
│ │   ├─attributes: AttributeListSyntax
│ │   ├─name: identifier("Value")
│ │   ├─colon: colon
│ │   ╰─inheritedType: IdentifierTypeSyntax
│ │     ╰─name: identifier("Comparable")
│ ╰─rightAngle: rightAngle
├─signature: FunctionSignatureSyntax
│ ├─parameterClause: FunctionParameterClauseSyntax
│ │ ├─leftParen: leftParen
│ │ ├─parameters: FunctionParameterListSyntax
│ │ │ ├─[0]: FunctionParameterSyntax
│ │ │ │ ├─attributes: AttributeListSyntax
│ │ │ │ ├─modifiers: DeclModifierListSyntax
│ │ │ │ ├─firstName: identifier("dump")
│ │ │ │ ├─secondName: identifier("a")
│ │ │ │ ├─colon: colon
│ │ │ │ ├─type: IdentifierTypeSyntax
│ │ │ │ │ ├─name: identifier("KeyPath")
│ │ │ │ │ ╰─genericArgumentClause: GenericArgumentClauseSyntax
│ │ │ │ │   ├─leftAngle: leftAngle
│ │ │ │ │   ├─arguments: GenericArgumentListSyntax
│ │ │ │ │   │ ├─[0]: GenericArgumentSyntax
│ │ │ │ │   │ │ ├─argument: IdentifierTypeSyntax
│ │ │ │ │   │ │ │ ╰─name: identifier("Root")
│ │ │ │ │   │ │ ╰─trailingComma: comma
│ │ │ │ │   │ ╰─[1]: GenericArgumentSyntax
│ │ │ │ │   │   ╰─argument: IdentifierTypeSyntax
│ │ │ │ │   │     ╰─name: identifier("Value")
│ │ │ │ │   ╰─rightAngle: rightAngle
│ │ │ │ ╰─trailingComma: comma
│ │ │ ├─[1]: FunctionParameterSyntax
│ │ │ │ ├─attributes: AttributeListSyntax
│ │ │ │ ├─modifiers: DeclModifierListSyntax
│ │ │ │ ├─firstName: identifier("b")
│ │ │ │ ├─colon: colon
│ │ │ │ ├─type: IdentifierTypeSyntax
│ │ │ │ │ ╰─name: identifier("String")
│ │ │ │ ╰─trailingComma: comma
│ │ │ ╰─[2]: FunctionParameterSyntax
│ │ │   ├─attributes: AttributeListSyntax
│ │ │   ├─modifiers: DeclModifierListSyntax
│ │ │   ├─firstName: identifier("c")
│ │ │   ├─colon: colon
│ │ │   ╰─type: IdentifierTypeSyntax
│ │ │     ╰─name: identifier("Int")
│ │ ╰─rightParen: rightParen
│ ├─effectSpecifiers: FunctionEffectSpecifiersSyntax
│ │ ╰─throwsClause: ThrowsClauseSyntax
│ │   ╰─throwsSpecifier: keyword(SwiftSyntax.Keyword.throws)
│ ╰─returnClause: ReturnClauseSyntax
│   ├─arrow: arrow
│   ╰─type: IdentifierTypeSyntax
│     ╰─name: identifier("Bool")
╰─body: CodeBlockSyntax
  ├─leftBrace: leftBrace
  ├─statements: CodeBlockItemListSyntax
  │ ╰─[0]: CodeBlockItemSyntax
  │   ╰─item: PrefixOperatorExprSyntax
  │     ├─operator: prefixOperator("!")
  │     ╰─expression: MemberAccessExprSyntax
  │       ├─base: DeclReferenceExprSyntax
  │       │ ╰─baseName: identifier("b")
  │       ├─period: period
  │       ╰─declName: DeclReferenceExprSyntax
  │         ╰─baseName: identifier("isEmpty")
  ╰─rightBrace: rightBrace

代码看起来很长,似乎有点儿“一望无际”。不过如果我们查看它的 description 属性就会恍然大悟:它其实就是 @nilable 宏修饰方法所对应的语法树。

在这里插入图片描述

所以基本上来说,我们只需要将 declaration 的内容“修剪”为想要的结果即可。对于我们这个例子,就是将 declaration 中第一个 KeyPath 参数中的 Value 变为可选类型 Value?。

下面是我们第一种实现,因为考虑到了“调皮的”用户可能犯的各种错误,所以比较冗长。不过别急,后面我们会给出精简后的方案:

public static func expansion(of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [DeclSyntax] {
        
    guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else {
        throw MacroExpansionErrorMessage("必须在方法上使用我哦!")
    }
    
    let funcName = funcDecl.name
    let funcModifiers = funcDecl.modifiers
    let funcGenericParameterClause = funcDecl.genericParameterClause
    let funcEffecSpecifier = funcDecl.signature.effectSpecifiers?.description ?? ""
    let funcReturnClause = funcDecl.signature.returnClause?.description ?? ""
    let funcBody = funcDecl.body
    
    guard let funcFirstParamenter = funcDecl.signature.parameterClause.parameters.first else {
        throw MacroExpansionErrorMessage("方法必须至少要有 1 个参数哦!")
    }
    
    guard let funcFirstParamenterType = funcFirstParamenter.type.as(IdentifierTypeSyntax.self) else {
        throw MacroExpansionErrorMessage("方法参数格式错误哦!")
    }
    
    guard funcFirstParamenterType.name.description == "KeyPath" else {
        throw MacroExpansionErrorMessage("方法的第一个参数必须是 KeyPath 类型哦!")
    }
    
    guard let secondArgment = funcFirstParamenterType.genericArgumentClause?.arguments.last, secondArgment.argument.as(OptionalTypeSyntax.self) == nil else {
        throw MacroExpansionErrorMessage("方法 KeyPath 参数中的 Value 不是 Optional 类型哦!")
    }
    
    let firstName = funcFirstParamenter.firstName.description
    
    var firstArg = if let secondName = funcFirstParamenter.secondName?.description {
        "\(firstName)\(secondName): "
    } else {
        "\(firstName): "
    }
    
    let rootName = funcFirstParamenterType.genericArgumentClause!.arguments.first!.argument.as(IdentifierTypeSyntax.self)!.name.description
    let valueName = secondArgment.argument.as(IdentifierTypeSyntax.self)!.description
    
    firstArg += "KeyPath<\(rootName), \(valueName)?>"
    
    let parameters = funcDecl.signature.parameterClause.parameters
    var otherParametersDesc = ""
    if parameters.count > 1 {
        for parameter in parameters.dropFirst() {
            otherParametersDesc += "\(parameter.description)"
        }
    }
            
    let nilFuncDecl = try FunctionDeclSyntax("\(funcModifiers)func \(funcName)\(funcGenericParameterClause)(\(raw: firstArg), \(raw: otherParametersDesc)) \(raw: funcEffecSpecifier)\(raw: funcReturnClause) \(funcBody)")
    
    return [.init(nilFuncDecl)]
}

在上面的代码中,我们做了这样几件事:

  • 为用户在构造宏所修饰的表达式时可能犯的各种错误(比如用户在属性而不是方法上应用 @nilable 宏)提供贴心的提示;
  • 确定 KeyPath 中 Value 的名称;
  • 考虑到 KeyPath 参数可能出现的名称前缀;
  • 确定方法签名中的其它信息(比如其它参数类型和返回类型);
  • 考虑到方法本身可能存在一些 modifier 和 effectSpecifiers 修饰器(比如 static、private、throws 等);

5.5 验证宏展开后的结果

回到 main.swift 源代码文件中,在选中 @nilable 宏关键字后将其展开,如果不出意外我们应该可以正确创建 sortItemsBy 排序方法的 KeyPath 可选 Value 版本(Value?):

在这里插入图片描述

这意味着,下面这些代码都可以顺利编译通过了:

let itemsByName = try! model.sortItemsBy(keyPath: \.name)
let itmesByNickname = try! model.sortItemsBy(keyPath: \.nickname)

棒棒哒!成就感爆棚的小伙伴们赶快给自已一个大大的赞吧!👍🏻


其实验证 Swift 宏展开结果最好的方法是使用单元测试,但由于目前宏单元测试略显“呆滞和笨拙”,所以暂时略去不表。


6. 另一个简洁的解决方案

虽然我们解决了问题,但上述实现实在是有点“臃肿不堪”。

让我们马上再做一次头脑风暴:我们实际只是想修改 sortItemsBy() 方法 KeyPath 形参中的 Value,如果只考虑几种可能出现的错误,仅需一个简单的字符串内容替换操作即可大功告成!

下面是我们 expansion() 方法简化后的新实现:

public static func expansion(of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [DeclSyntax] {
    guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else {
        throw MacroExpansionErrorMessage("必须在方法上使用我哦!")
    }
    
    var funcDesc = funcDecl.description
    funcDesc.replace(/@nilable/, with: "")
    
    let valueFinder = /.*KeyPath<.+?,\s*(.+)>,/
    
    guard let value = try valueFinder.firstMatch(in: funcDesc)?.output.1 else {
        throw MacroExpansionErrorMessage("找不到 KeyPath !")
    }

    funcDesc.replaceSubrange(value.startIndex..<value.endIndex, with: "\(value)?")    
    return [.init(try FunctionDeclSyntax("\(raw: funcDesc)"))]
}

在上面的代码中,我们只是仅仅替换原方法里形参 KeyPath 中的值类型 Value 为 Value? 而已,真是简单的不要不要的。

验证 @nilable 宏展开分结果,和之前是一毛一样的,棒棒哒!💯

7. 目前 Swift 宏的不足之处

虽然 Swift Macros 更好地弥补了 Swift 语言动态性不足的“先天缺陷”,让秃头码农们更优雅的遵循 “KISS” 原则,避免了代码重复。

但是,目前 Swift 宏仍有一些白圭之玷,它们主要表现在如下几个方面:

  • 无论我们是否喜欢,即使一个非常简单的宏实现都需要放在 Swift Package 中;
  • 官方示例很少,研究起来比较容易掉头发;
  • 定义 7 种宏的“隐藏”选项几乎没有文档,更容易掉头发;
  • FunctionDeclSyntax 等一些声明语法实体都是只读的,不太灵活;
  • 虽然像 FunctionDeclSyntax 这些声明语法实体都包含 ResultBuilder 构造器,但实际无法像 SwiftUI 那样自由组合内部元素;
  • 因为要实现庞大 Swift 语言的类型安全,所以 Swift 宏背后语法树的结构也很复杂,经常云里雾里的感觉;
  • 貌似宏的实现有缓存,所以在 Xcode 16 中修改宏的展开代码有时不能及时触发测试用例中宏展开结果的刷新,需要重启 Xcode;

以上只是其中一部分“美中不足”,其它一些小问题并没有完全列出来。

其实,现在 Swift 宏最大的问题就是它缺乏文档而且太复杂了,这就是为什么目前没什么人用的原因。

希望苹果在 WWDC 25 的 Swift Macros 2.0 中(如果可能的话)可以简化和改善它。

即便如此,雪中送炭的 Swift 宏仍然给了我们太多惊喜和便利,值得小伙伴们进一步深入挖掘。

本系列文章至此告一段落了,有机会我们会单写几篇 Swift 宏的进阶博文,敬请期待吧!

总结

在本篇博文中,我们介绍了宏展开方法中 declaration 参数的构成(FunctionDeclSyntax ),并详细讨论了自定义宏主体的实现;我们随后还精简了宏的展开逻辑并顺便聊了聊当前 Swift 宏的一些不足之处。

感谢观赏,再会啦!😎


http://www.kler.cn/a/421381.html

相关文章:

  • 微软表示不会使用你的 Word、Excel 数据进行 AI 训练
  • MATLAB —— 机械臂工作空间,可达性分析
  • Linux下,用ufw实现端口关闭、流量控制(二)
  • Springboot(四十九)SpringBoot3整合jetcache缓存
  • 使用SpringBoot实现邮件发送(QQ邮箱为例)
  • 【Robocasa】Code Review
  • 【Anaconda】Anaconda3 下载与安装教程(Ubuntu 22.04)
  • SQL语法——DDL
  • 一键生成后端服务,MemFire Cloud重新定义开发效率
  • 基于Matlab SEIR模型的传染病传播分析与防控策略模拟
  • Leetcode 每日一题 205.同构字符串
  • 114. UE5 GAS RPG 实现配置怪物生成
  • 量化交易系统开发-实时行情自动化交易-8.7.文华平台
  • 【开源】A060-基于Spring Boot的游戏交易系统的设计与实现
  • 从0开始学PHP面向对象内容之常用设计模式(中介,访问)
  • 晶圆制程setup的一些探讨
  • python笔记3
  • 单元测试报websocket bean创建失败
  • (1)spring security - 项目环境搭建及入门
  • 群控系统服务端开发模式-应用开发-短信工厂华为云短信开发
  • 警惕开源信息成为泄密源头
  • Android 硬件抽象层(HAL)全解析:智能设备硬件协同揭秘
  • 论文阅读之方法: Single-cell transcriptomics of 20 mouse organs creates a Tabula Muris
  • 3GPP R18 LTM(L1/L2 Triggered Mobility)是什么鬼?(三) RACH-less LTM cell switch
  • js 字符串 只显示数字
  • 网络安全技术详解:虚拟专用网络(VPN) 安全信息与事件管理(SIEM)