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.namelet funcModifiers = funcDecl.modifierslet funcGenericParameterClause = funcDecl.genericParameterClauselet funcEffecSpecifier = funcDecl.signature.effectSpecifiers?.description ?? ""let funcReturnClause = funcDecl.signature.returnClause?.description ?? ""let funcBody = funcDecl.bodyguard 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.descriptionvar firstArg = if let secondName = funcFirstParamenter.secondName?.description {"\(firstName)\(secondName): "} else {"\(firstName): "}let rootName = funcFirstParamenterType.genericArgumentClause!.arguments.first!.argument.as(IdentifierTypeSyntax.self)!.name.descriptionlet valueName = secondArgment.argument.as(IdentifierTypeSyntax.self)!.descriptionfirstArg += "KeyPath<\(rootName), \(valueName)?>"let parameters = funcDecl.signature.parameterClause.parametersvar 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.descriptionfuncDesc.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 宏的一些不足之处。
感谢观赏,再会啦!😎
相关文章:

Swift 宏(Macro)入门趣谈(五)
概述 苹果在去年 WWDC 23 中就为 Swift 语言新增了“其利断金”的重要小伙伴 Swift 宏(Swift Macro)。为此,苹果特地用 2 段视频(入门和进阶)颇为隆重的介绍了它。 那么到底 Swift 宏是什么?有什么用&…...
ES6 Set、Map、WeakSet、WeakMap 四者辨析与实战应用详解
在 ES6 中,Set 和 Map 是两种非常重要的新增数据结构,它们都具有独特的特性和用途,能够帮助开发者更高效地处理和管理数据。除此之外,WeakSet 和 WeakMap 作为这两种数据结构的变种,也具有一些特殊的功能。下面我会从 Set 数据结构、Map 数据结构、WeakSet 和 WeakMap 对比…...

【数据结构】哈希表实现
前言 在本篇博客中,作者将会带领你使用C语言来实现一个哈希表。 一.什么是哈希表 在实现哈希表之前,我们先来学习一下什么是哈希表。 在传统的数据结构中,例如数组,链表和二叉平衡树等数据结构,这些数据结构的元素关键…...
Verilog的线与类型与实例化模块
1、线与类型 在Verilog中,线与(wire-AND)类型通常用于描述多个信号进行逻辑与(AND)操作的电路行为。虽然Verilog本身没有直接定义一种名为“线与”的数据类型,但可以通过使用wire类型结合特定的逻辑操作来…...

芯片测试-RF中的S参数,return loss, VSWR,反射系数,插入损耗,隔离度等
RF中的S参数,return loss, VSWR,反射系数,插入损耗,隔离度 💢S参数💢💢S11与return loss,VSWR,反射系数💢💢S21,插入损耗和增益&#…...

强化学习的几个主要方法(策略梯度、PPO、REINFORCE实现等)(上)
本笔记有大量参考蘑菇书EasyRL https://datawhalechina.github.io/easy-rl/#/ 包括其配图和部分文本。 1. 基本概念 1.1 基本流程 强化学习是一种学习框架,其中智能体(Agent) 通过与 环境(Environment) 的交互&#…...

Git远程仓库操作
文章目录 远程仓库连接Gitee克隆代码 多人协同问题说明 🏡作者主页:点击! 🤖Git专栏:点击! ⏰️创作时间:2024年12月1日13点10分 远程仓库 Git 是分布式版本控制系统,同一个 Git …...

GAGAvatar: Generalizable and Animatable Gaussian Head Avatar 学习笔记
1 Overall GAGAvatar(Generalizable and Animatable Gaussian Avatar),一种面向单张图片驱动的可动画化头部头像重建的方法,解决了现有方法在渲染效率和泛化能力上的局限。 旋转参数 现有方法的局限性: 基于NeRF的方…...
什么是VISUAL STUDIO CODE (V S CODE)
Visual Studio Code(简称VS Code)是由微软开发的一个免费的、开源的源代码编辑器。它是一个轻量级但功能强大的工具,支持多种编程语言和框架,广泛用于开发各种应用程序,尤其是Web开发。VS Code具备以下特点:…...
2024年09月中国电子学会青少年软件编程(Python)等级考试试卷(三级)答案 + 解析
青少年软件编程(Python)等级考试试卷(三级) 分数:100 题数:38 一、单选题(共25题,共50分) 1. 以下表达式的值为True的是?( ) A. all( ,1,2,3) B. any([]) C. bool(abc) D. divmod(6,0)...

C++初阶——动态内存管理
目录 1、C/C内存区域划分 2、C动态内存管理:malloc/calloc/realloc/free 3、C动态内存管理:new/delete 3.1 new/delete内置类型 3.2 new/delete自定义类型 4、operator new与operator delete函数 5、new和delete的实现原理 5.1 内置类型 5.2 自定…...

如何查看阿里云ddos供给量
要查看阿里云上的 DDoS 攻击量,你可以通过阿里云的 云盾 DDoS 防护 服务来进行监控和查看攻击数据。阿里云提供了详细的流量监控、攻击日志以及攻击趋势分析工具,帮助用户实时了解 DDoS 攻击的情况。以下是九河云总结的查看 DDoS 攻击量的步骤࿱…...
MySQL中的事务隔离全详解
第一部分:MySQL事务的特性与并行事务引发的问题 1. 什么是事务及其四大特性(ACID)? 事务(Transaction)是数据库操作的基本单位,它将一组操作组合在一起,以确保这些操作作为一个整体…...

异常--C++
文章目录 一、异常的概念及使用1、异常的概念2、异常的抛出和捕获3、栈展开4、查找匹配的处理代码5、异常重新抛出6、异常安全问题7、异常规范 二、标准库的异常 一、异常的概念及使用 1、异常的概念 异常处理机制允许程序中独立开发的部分能够在运行时就出现的问题进行通信并…...

SeggisV1.0 遥感影像分割软件【源代码】讲解
在此基础上进行二次开发,开发自己的软件,例如:【1】无人机及个人私有影像识别【2】离线使用【3】变化监测模型集成【4】个人私有分割模型集成等等,不管是您用来个人学习还是公司研发需求,都相当合适,包您满…...
锁-读写锁-Swift
实现一 pthread_mutex_t: ReadWriteLock/Sources/ReadWriteLock at main SomeRandomiOSDev/ReadWriteLock GitHub https://swiftpackageindex.com/reers/reerkit/1.0.39/documentation/reerkit/readwritelock/ // // Copyright © 2022 reers. // // Pe…...

Kafka如何保证消息可靠?
大家好,我是锋哥。今天分享关于【Kafka如何保证消息可靠?】面试题。希望对大家有帮助; Kafka如何保证消息可靠? 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 Kafka通过多种机制来确保消息的可靠性,主要包…...

5.10【机器学习】
如果FLAG的画,就是已经有模型了,不然就新建一个模型,通过TORCH方法 在训练的时候,如果TRAIN的话就是训练,不然就是预测 forward前向预测出来一个结果,就是1234 在train方法里,进行多轮迭代&am…...

[白月黑羽]关于仿写股票数据软件题目的解答
原题: 对应问题视频: 实现的效果 不同点 实现的作品和原题要求的不同点 题目要求爬虫获取数据,作品中是调库获取所有股票历史数据实时数据使用爬虫的方式爬取指定股票的数据,需要实时更新,我做了修改,改…...

详解LZ4文件解压缩问题
详解LZ4文件解压缩问题 一、LZ4文件解压缩方法1. 使用LZ4命令行工具2. 使用Python库3. 使用第三方工具4. 在线解压工具 二、常见问题及解决方法1. 解压显示文件损坏2. 解压后文件大小异常 三、总结 LZ4是一种快速的压缩算法,广泛应用于需要实时压缩和解压缩大文件的…...

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式
一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明:假设每台服务器已…...

多云管理“拦路虎”:深入解析网络互联、身份同步与成本可视化的技术复杂度
一、引言:多云环境的技术复杂性本质 企业采用多云策略已从技术选型升维至生存刚需。当业务系统分散部署在多个云平台时,基础设施的技术债呈现指数级积累。网络连接、身份认证、成本管理这三大核心挑战相互嵌套:跨云网络构建数据…...
HTML 语义化
目录 HTML 语义化HTML5 新特性HTML 语义化的好处语义化标签的使用场景最佳实践 HTML 语义化 HTML5 新特性 标准答案: 语义化标签: <header>:页头<nav>:导航<main>:主要内容<article>&#x…...

RocketMQ延迟消息机制
两种延迟消息 RocketMQ中提供了两种延迟消息机制 指定固定的延迟级别 通过在Message中设定一个MessageDelayLevel参数,对应18个预设的延迟级别指定时间点的延迟级别 通过在Message中设定一个DeliverTimeMS指定一个Long类型表示的具体时间点。到了时间点后…...
Go 语言接口详解
Go 语言接口详解 核心概念 接口定义 在 Go 语言中,接口是一种抽象类型,它定义了一组方法的集合: // 定义接口 type Shape interface {Area() float64Perimeter() float64 } 接口实现 Go 接口的实现是隐式的: // 矩形结构体…...

【Redis技术进阶之路】「原理分析系列开篇」分析客户端和服务端网络诵信交互实现(服务端执行命令请求的过程 - 初始化服务器)
服务端执行命令请求的过程 【专栏简介】【技术大纲】【专栏目标】【目标人群】1. Redis爱好者与社区成员2. 后端开发和系统架构师3. 计算机专业的本科生及研究生 初始化服务器1. 初始化服务器状态结构初始化RedisServer变量 2. 加载相关系统配置和用户配置参数定制化配置参数案…...

《通信之道——从微积分到 5G》读书总结
第1章 绪 论 1.1 这是一本什么样的书 通信技术,说到底就是数学。 那些最基础、最本质的部分。 1.2 什么是通信 通信 发送方 接收方 承载信息的信号 解调出其中承载的信息 信息在发送方那里被加工成信号(调制) 把信息从信号中抽取出来&am…...

全志A40i android7.1 调试信息打印串口由uart0改为uart3
一,概述 1. 目的 将调试信息打印串口由uart0改为uart3。 2. 版本信息 Uboot版本:2014.07; Kernel版本:Linux-3.10; 二,Uboot 1. sys_config.fex改动 使能uart3(TX:PH00 RX:PH01),并让boo…...
CMake控制VS2022项目文件分组
我们可以通过 CMake 控制源文件的组织结构,使它们在 VS 解决方案资源管理器中以“组”(Filter)的形式进行分类展示。 🎯 目标 通过 CMake 脚本将 .cpp、.h 等源文件分组显示在 Visual Studio 2022 的解决方案资源管理器中。 ✅ 支持的方法汇总(共4种) 方法描述是否推荐…...

AI病理诊断七剑下天山,医疗未来触手可及
一、病理诊断困局:刀尖上的医学艺术 1.1 金标准背后的隐痛 病理诊断被誉为"诊断的诊断",医生需通过显微镜观察组织切片,在细胞迷宫中捕捉癌变信号。某省病理质控报告显示,基层医院误诊率达12%-15%,专家会诊…...