大多数浏览器和
Developer App 均支持流媒体播放。
-
深入了解 Swift 宏
探索如何使用 Swift 宏在你的代码库中减少样板代码,并更轻松地采用复杂功能。了解宏如何分析代码,生成丰富的编译器错误以引导开发者正确使用宏,以及生成自动合并回项目中的新代码。我们还将介绍一些重要概念,如宏角色、编译器插件和语法树。
章节
- 0:00 - Introduction
- 0:51 - Why macros?
- 2:13 - Design philosophy
- 4:48 - Translation model
- 6:18 - Macro roles
- 17:48 - Macro implementation
- 33:36 - Writing correct macros
- 38:42 - Wrap up
资源
相关视频
WWDC23
-
下载
♪ ♪
Becca:嗨 我是来自 Swift 团队的 Becca 今天我将介绍 Swift 宏 这是一项令人兴奋的新功能 宏可以让开发者根据需要 自定义 Swift 语言 我们首先会讨论宏的用途 然后 我将介绍 我们在设计 Swift 宏时 考虑的一些原则 接下来 我将介绍 Swift 宏的工作原理 以及它们与你项目中的其他代码 交互的具体方式
接着我们会讨论如何实现宏 并在本讲座最后讨论 如何确保你的宏能正常工作
那么我们首先来谈谈 Swift 为什么支持宏 Swift 希望让用户编写 易于理解的代码和 API 这就是为什么它提供了 派生一致性和结果构建器等功能 帮助用户避免 编写重复的样板代码 这些功能的工作方式 基本上都相同 例如 当你遵守 Codable 而不为其成员 提供实现时 Swift 会自动将一致性展开为 一组插入程序中的成员 我们可以在灰色框中 看到一致性的展开 创建这段代码让你可以使用 Codable 而无需深入了解 其工作原理 并且不需要决定 添加 Codable 支持 是否值得编写一大堆代码 Swift 有很多功能 是以这种方式工作的 开发者只要编写一些简单的语法 编译器就会自动 将其展开成更一段复杂的代码 但当现有的功能无法满足 开发者的需要时该怎么办呢? 开发者当然可以 向 Swift 编译器添加一个功能 因为它是开源的 但这样实际上需要我本人 亲自参加视频会议 并与其他 Swift 项目负责人 讨论你的功能 所以这并不是一个可以扩展的过程 因此我们引入了宏 宏可以让开发者向 Swift 中 添加自己的语言特性 消除枯燥和样板代码 而且你可以在 Swift 包中进行分发 而无需修改编译器 有些开发者可能没有 在其他语言中使用过宏 但如果你使用过 可能对它们心情很复杂 部分原因是许多 Swift 开发者熟悉 Objective-C 或其他 使用 C 预处理器的语言 并且了解 C 宏的限制和陷阱 但 Swift 宏和它们有很大的不同 可以避免许多这些问题 我们在设计宏时考虑了四个目标 第一个目标是当开发者在使用宏时 它应该非常容易找到 宏有两种类型: 独立宏用于替代代码中的其他内容 它们始终以井号 (#) 开头 附加宏用于 在代码中的声明上添加属性 它们始终以 at (@) 符号开头 Swift 已经使用井号 (#) 和 at 符号 (@) 表示特殊的编译器行为 宏只是使其具有可扩展性 所以如果开发者 没有看到任何 # 和 @ 符号 你就可以确定没有涉及任何宏 第二个目标是宏的输入代码 和输出代码都应该是完整的 并且需要进行错误检查 开发者不能将“1 +”传递给宏 因为参数必须是完整的表达式 同样 开发者也不能 传递错误类型的参数 因为宏的参数和结果 都会进行类型检查 就像函数的参数一样 宏的实现可以验证其输入 并在出现问题时 发出编译器警告或错误 因此开发者可以更容易确定 是否正确使用了宏 第三个目标是宏的展开应该以 可预测、仅添加的方式插入程序中 宏只能增加程序中的可见代码 而不能删除或修改代码 所以即使我们不知道 “someUnknownMacro”能干什么 我们仍然可以确定它不会删除 对“finishDoingThingy”的调用 或将其移动到新函数中 这使得阅读使用宏的代码 变得更加容易 而最后一个目标就是 宏不应该是看不穿的魔法 宏只是在程序中添加了更多代码 而开发者可以在 Xcode 中 直接看到这一过程 开发者可以右击宏的使用位置 并请求查看其展开内容 你也可以在展开内容中设置断点 或者直接使用调试器介入 当宏展开中的代码无法编译时 你可以同时看到 展开内容中的错误位置 以及该展开内容在源代码中的位置 即使宏是由闭源库提供的 所有这些工具 也可以工作 宏作者甚至可以为宏编写单元测试 以确保它们按预期工作 我们强烈建议你这样做 我们认为这些目标可以让开发者 更容易理解和维护 Swift 宏 现在我们明白了 Swift 宏要实现的目标 让我们来谈谈它们是怎么做到的 在沉迷于细节之前 先让我们掌握基本概念 当 Swift 看到 开发者在代码中调用宏时 比如 Xcode 宏包模板中的 “stringify”宏 它会从代码中提取该使用 并将其发送到一个包含 该宏实现的特殊编译器插件 该插件作为一个独立的进程 在一个安全的沙盒中运行 并且它包含由宏作者 编写的自定义 Swift 代码 它会处理宏的使用 并返回一个“展开” 即由该宏创建的新代码片段 然后 Swift 编译器会将该展开 添加到开发者的程序中 并将代码和展开一起编译 因此当开发者运行程序时 它的工作方式就像是 开发者自己编写了展开 而不是调用宏一样 但是我刚才忽略了很重要的一点 Swift 是如何知道 “stringify”宏存在的呢? 答案是 它来自于宏声明 宏声明为宏提供了 API 开发者可以直接 在自己的模块中编写声明 也可以从库或框架中导入 它指定了宏的名称和签名 它所接受的参数数量、标签和类型 以及宏的返回类型 如果宏有的话 就像函数声明一样 它还可以有一个或多个属性 用于指定宏的角色 如果要编写一个宏 就必须考虑它的角色是什么 因此 让我们来谈谈什么是角色 以及如何使用不同的角色 来编写不同类型的宏 角色是宏的一组规则 它规定了宏的应用位置和方式 宏展开成什么样的代码 以及展开插入到代码中的位置 最终 是宏角色负责 实现我们的目标 即以可预测、 仅添加的方式插入展开 有两种角色可以创建独立宏: 表达式和声明 还有五种角色可以创建附加宏: 对等、访问器、 成员属性、成员以及一致性 让我们来看看这些角色 以及什么时候可能会用它们 我们先从“独立表达式”角色开始 或许你对 “表达式”这个术语不太熟悉 表达式就是我们所说的 执行并产生结果的代码单元 在这个“let”语句中 等号后面的算式就是一个表达式 但是表达式具有递归结构 它们通常由更小的表达式组成 因此“x + width”单独来看 也是一个表达式 “width”这个词也是表达式 因此“独立表达式”宏 就是一个可以展开为表达式的宏 那么开发者要如何使用它呢? 假如你需要强制解包一个可选项目 Swift 提供强制解包运算符 但有些团队认为 在不考虑其安全性的情况下 引入强制解包有些太简单了 因此他们的编码规范要求开发者 写一些更复杂的东西 来说明为什么 这个值永远不能是 nil 但是大多数替代方案 比如使用“guard let” 然后在“else”分支中 调用“preconditionFailure” 又显得有点太过隆重了 让我们设计一个宏 来平衡这两种极端 我们希望这个宏 能够计算并返回一个值 所以我们将其设为 “freestanding(expression)”宏 我们将其命名为“unwrap” 并设置为泛型类型 其中传入的值是可选的 但返回的值不是可选的 同时我们还传入了一个字符串 作为在解包失败时 打印的消息的一部分 因此 我们最终得到一个 像函数一样调用的宏 但会它展开为一个表达式 其中包含一个 用闭包包裹的“guard let” 错误消息中甚至还包括变量名 这在普通函数中是不可能实现的 现在我们已经认识了 独立表达式角色 让我们来看看独立声明角色 它可以展开为一个或多个声明 比如函数、变量或类型 那么开发者可以用它做什么呢? 想象一下你正在编写某种需要 二维数组类型的统计分析 你希望数组中的所有行 都具有相同数量的列 因此你不需要“数组的数组”
相反 你希望将元素存储在 一个扁平的一维数组中 然后根据开发者传入的 二维索引计算出一个一维索引 为此 你可以编写一个像这样的类型 “makeIndex”函数接受 用于二维索引的两个整数 然后进行一点儿算数 将它们转换为一维索引 但随后你又发现 在程序的另一个地方 你需要一个三维数组 和之前的二维数组几乎完全一样 只是多了几个索引 计算也更复杂一些 接着你又需要四维数组、五维数组 很快你就会淹没在 几乎相同的数组类型中 但它们又不完全一样 以致于你无法使用泛型、协议扩展、 子类 或 Swift 提供的 其他用于此类问题的功能 但幸运的是 所有这些结构体都是声明 所以我们可以使用 声明宏来创建它们 那么让我们声明一个独立声明宏 名为“makeArrayND” 因为它将创建一个 N 维数组类型 我们将通过一个 Int 参数 传递维度的数量 并且不会声明结果类型 因为该宏会向我们的程序添加声明 而不会计算其他代码使用的结果 现在我们可以用二维、 三维、四维和五维调用这个宏四次 每次调用都将展开为 一个完整的多维数组类型 并具有正确数量的参数 和该大小的正确计算 目前为止 我们只介绍了独立宏 现在让我们来看看附加宏的角色 顾名思义 附加宏 可以附加到特殊的声明上 这意味着它们有更多的信息可以使用 独立宏只能使用它们接受的参数 但附加宏还可以访问它们附加的声明 它们经常检查该声明 并从中提取名称、 类型和其他信息 我们先从附加对等角色开始 对等宏可以附加到任何声明上 不仅仅是变量、函数和类型 甚至包括导入和运算符声明 并且可以在它旁边插入新的声明 因此如果你将这个宏 附加到方法或属性上 那么它就可以生成该类型的成员 如果你将它附加到 顶层函数或类型上 它会生成新的顶层声明 这让它变得非常灵活 开发者可以这样使用它 假设你正在编写一个 使用 Swift Concurrency 的库 但你知道有些客户 仍在使用旧的并发技术 因此你想给他们提供使用 完成处理程序的 API 版本 编写这些方法并不难 你只需删除“async”关键字 添加完成处理程序参数 将结果类型移到参数列表中 然后在分离的任务中 调用异步版本即可 但你需要这样做很多次 而且你不想手动编写这些代码 这个任务就很适合附加对等宏 我们将声明一个名为 “AddCompletionHandler”的宏 并为其提供一个 完成处理程序的实参标签的形参 然后将该宏附加到 该方法的异步版本 该宏将创建一个与原始签名等效的 基于完成处理程序的签名 并编写方法主体 甚至为完成处理程序附加 包含额外文本的文档注释 很酷吧? 接下来我们看看附加访问器角色 它们可以附加到变量和下标上 并为其添加访问器 如“get”、 “set”、“willSet”或“didSet” 那么这有什么用处呢? 假设你有一堆 基本上包装字典的类型 并且允许你通过属性 访问它们的内容 比如这个结构体“Person”允许你访问 “name”、“height” 和“birth_date”字段 但是如果字典中 除了这三个字段之外还有其他信息 它们就会被你的程序保留并忽略掉 这三个属性需要 计算的 getter 和 setter 但是手动编写它们很繁琐 而且我们不能使用属性包装器 因为属性包装器无法访问 它们所使用的 类型上的其他存储属性 那么让我们编写一个 附加访问器宏来解决这个问题 我们将其命名为 “DictionaryStorage” 我们将给它一个“key”参数 因为字典拼写的“birth_date” 带有下划线 但你也可以将键值省略 它会默认为 nil 这样宏就会 使用属性的名称作为键值 现在 你不需要再编写 庞大的访问器代码块 你只需在每个属性前加上 “@DictionaryStorage” 宏将为你生成访问器 这个改进很不错 但这里还存在一些样板代码 那就是相同的 “DictionaryStorage”属性 虽然它们没有那么样板 但它们仍然是样板代码 你可以通过将一些内置属性应用于 整个类型或扩展来处理这种情况 “附加成员属性”角色 也能让你的宏变得像这样 这个宏可以附加到类型或扩展上 并且它可以为 附加的任何成员添加属性 让我们来看看如何实现 这次我们做些不一样的事情 我们不会声明一个新的宏 而是在“DictionaryStorage”宏 已经拥有了的 “附加访问器”角色之外 为它添加另一个角色属性 这是一种非常有用的创建宏的技术 除了两个独立角色外 开发者可以组合任意宏角色 因为在某些情况下 Swift 无法确定使用哪一个 Swift 会将所有角色进行 合适的展开 不论你在哪里应用它们 但其中要至少 有一个角色适用于该位置 所以 如果你将“DictionaryStorage” 附加到一个类型上 Swift 会展开“成员属性”角色 如果你将其附加到一个属性上 Swift 将展开“访问器”角色 但如果你将它附加到函数上 将会出现编译错误 因为“DictionaryStorage”没有任何 可以附加到函数上的角色
将第二个角色添加到 “DictionaryStorage”后 你可以将其附加到整个类型 而不是将其单独附加到每个属性 这个宏的逻辑将跳过某些成员 比如初始化程序、“dictionary”属性 以及像“birth_date”这样 已经有了 “DictionaryStorage”属性的属性 但它会为任何其他存储属性 添加“DictionaryStorage”属性 然后这些属性将扩展为 我们之前看到的访问器 这个改进很棒 但我们 还有更多的样板代码可以消除 那就是初始化程序和储存的属性 它们是“DictionaryRepresentable” 协议所要求的 并且其属性由访问器使用 但它们在任何使用 DictionaryStorage 的类型中都是相同的 我们可以让 DictionaryStorage 宏自动添加它们 这样我们就不用手动编写了 我们可以用“附加成员”角色 来做到这一点 与成员属性宏类似 你可以将这些宏 应用于类型和扩展 但它们不是向现有成员 添加属性 而是添加全新的成员 因此你可以添加方法、 属性、初始化程序等等 你甚至可以向类和结构体 添加存储属性 或向枚举添加案例 我们将再次向 DictionaryStorage 宏添加一个 新的“附加成员”角色 并将它与之前两个角色相结合 这个新角色将添加一个初始化程序 和一个名为“dictionary”的属性
你或许会好奇 当两个不同的宏应用于相同的代码时 哪一个会先被展开? 答案是 这并不重要 每个宏都将看到原始版本的声明 而不会看到其他宏提供的展开 所以你无需担心顺序问题 无论编译器何时展开你的宏 你都会看到相同的结果 添加了附加成员角色后 我们甚至不需要 再手动编写这两个成员 只需在类型上使用 DictionaryStorage 它将自动为我们添加它们 然后 另一个角色将在属性上 添加 DictionaryStorage 属性 而这些属性将扩展为访问器 依此类推
但我们还有最后一小段 样板代码需要消除 即 DictionaryRepresentable 协议的一致性 “附加一致性”角色 用在这里再适合不过了 它可以为类型或扩展添加一致性 现在我们为 “DictionaryStorage”宏添加最后的 “附加一致性”角色 将其与其他三个角色组合在一起 这个新的角色将为 “DictionaryRepresentation”添加一致性 现在我们不需要手动编写一致性了 我们已经为访问器 和生成的成员添加了 DictionaryStorage 属性 现在除了它同时在做的其他事情 它也将自动添加一致性 我们在这部分已经讲了很久了 让我们来回顾一下 我们将一个巨大而杂乱的、 充满重复代码的类型中的 大部分代码 转移到一个超级强大的宏的 多个角色中 这样剩下的代码就能简明地表现 该特定类型的特殊之处 想象一下 如果你有 10 个或 20 个 可以使用 DictionaryStorage 的类型 那么处理它们将变得多么容易啊 我们已经花了很多时间 来讨论声明和角色 但到目前为止 它们所展开的代码 似乎只是奇迹般就出现了 现在让我们填补这个空白 并讨论要如何实现宏 之前我向你展示宏声明时 我遗漏了一些非常重要的东西: 宏的实现 它位于等号之后 而且它总是另一个宏 有时 它可能是你编写的另一个宏 只是参数重新排列了一下 或者将额外的参数指定为字面量 但通常情况下 你会使用外部宏 外部宏是 由编译器插件实现的一种宏 也许你还记得 我之前提到过编译器插件 我说过 当编译器看到正在使用宏时 它会在单独的进程中启动一个插件 并要求它展开该宏 “#externalMacro”就定义了这种关系 它指定了编译器应该启动的插件 以及插件中类型的名称 因此 当 Swift 展开这个宏时 它将启动一个名为 “MyLibMacros”的插件 并要求一个名为 “StringifyMacro”的类型来展开它 宏声明与其他 API 一起位于你的普通库中 但宏实现位于单独的 编译器插件模块中 而“#externalMacro” 在宏声明和实现它的 类型之间建立了链接 宏实现是什么样的呢? 让我们来看一下如何实现 DictionaryStorage 你或许还记得 “DictionaryStorage”宏 具有“附加成员”角色 它为类型添加了一个存储属性 和一个初始化程序 这里是该角色的简单实现 我们将逐步解释并学习其工作原理 我们首先在顶部导入一个 名为 SwiftSyntax 的库 SwiftSyntax 是由 Swift 项目 维护的一个包 它可以帮助你解析、 检查、操作和生成 Swift 源代码 随着 Swift 语言的发展 Swift 贡献者在不断更新 SwiftSyntax 因此它支持 Swift 编译器的所有功能 SwiftSyntax 将源代码表示为 一种特殊的树形结构 例如 在这段代码示例中的 结构体“Person” 被表示为一个名为 “StructDeclSyntax”的类型的实例 但是该实例具有属性 而且其中每个属性都代表 结构体声明的某个部分 属性列表位于“attributes”属性中 实际的关键字“struct” 位于“structKeyword”属性中 该结构体的名称位于 “identifier”属性中 带有大括号的主体和结构体的成员 位于“memberBlock”属性中 还有一些属性 比如“modifiers” 表示某些结构体声明所具有的属性 但是这个结构体没有 这些属性为 nil 这些属性中的一些语法节点 被称为“token” 它们代表源文件中的一段特定文本 例如名称、关键字或一些标点符号 并且只包含该文本 及其周围的空格和注释等 细枝末节的其他内容 如果你深入解析语法树 你会找到一个覆盖源文件 每个字节的 token 节点 但有些节点 例如“attributes”属性中的 “AttributeListSyntax”节点 和“memberBlock”属性中的 “MemberDeclBlockSyntax”节点 并不是 token 它们在自己的属性中有子节点 比方说 如果我们查看 “memberBlock”属性的内部 我们会发现一个 左侧大括号的 token 成员列表的 “MemberDeclListSyntax”节点 以及右侧大括号的 token 而如果我们继续查看 “MemberDeclListSyntax”节点内部 我们最终会找到 每个属性的节点 以此类推 SwiftSyntax 本身 就是一个庞大的主题 因此我不会让这个视频变得两倍长 而是向你推荐另外两个资源 一个是配套的“编写 Swift 宏”讲座 其中包含了一些实用技巧 用于了解特定源代码 是如何表示为语法树的 另一个资源是 SwiftSyntax 包的文档 你可以在网上找到它 或者如果你在宏包中 使用 Xcode 的 Build Documentation 命令 SwiftSyntax 文档将出现在 Developer Documentation 窗口中 除了主 SwiftSyntax 库之外 我们还导入了另外两个模块 一个是“SwiftSyntaxMacros” 它提供了编写宏所需的 协议和类型 另一个名为“SwiftSyntaxBuilder” 该库提供了方便的 API 用于构建语法树 以表示新生成的代码 不使用它也可以编写宏 但它非常方便好用 我们强烈推荐开发者利用它 现在我们已经导入了这些库 我们将开始实际编写 我们的插件应该提供的 “DictionaryStorageMacro”类型 请注意 它遵守名为 “MemberMacro”的协议 每个角色都有对应的协议 并且其实现必须遵守宏所提供的 每个角色的协议 “DictionaryStorage”宏 具有其中四个角色 因此“DictionaryStorageMacro” 类型需要遵守 四个相应的协议 但为了简单起见 我们现在只关注 成员宏的一致性 我们来看这个类型的主体 这里有一个叫 “expansion of, providingMembersOf, in.”的方法 这个方法是成员宏协议所需的 当使用宏时 Swift 编译器 会调用它来扩展成员角色 我们还没有使用这些参数 但稍后会讨论它们 现在 请注意它是一个静态方法 所有的展开方法都是静态的 所以 Swift 实际上并不创建 DictionaryStorageMacro 类型的实例 它只是将其用作方法的容器 每个展开方法都返回 SwiftSyntax 节点 这些节点会被插入到源代码中 成员宏会展开为一个声明列表 作为成员添加到类型中 因此成员宏的 展开方法返回一个 “DeclSyntax”节点的数组 如果我们查看主体内部 我们会看到该数组正在创建 它包括我们希望这个宏添加的 初始化程序和存储属性 现在这里的“var dictionary”部分 看起来像一个普通的字符串 但实际上并不是 这个字符串字面量被写入了 DeclSyntax 的预期位置 所以 Swift 实际上 将其视为源代码片段 并要求 Swift 解析器将其 转换为 DeclSyntax 节点 这是 SwiftSyntaxBuilder 库 提供的便利之一 还好我们之前导入了它 有了这些 再加上其他三个角色 协议的一致性 我们就有了 DictionaryStorage 宏的 有效实现 但是 尽管这个宏 在正确使用时可以工作 如果错误使用它会发生什么呢? 例如 如果你尝试将其应用于枚举 而不是结构体 会发生什么呢? “附加成员”角色将尝试 添加一个存储的“dictionary”属性 但是枚举不能有存储的属性 所以 Swift 会产生一个错误: “枚举不能包含存储属性” Swift 会阻止这段代码的编译 这样很棒 但这个错误信息 有点令人困惑 不是吗? 我们并不清楚为什么 DictionaryStorage 宏 试图创建一个存储属性 还有该怎么改正它 我之前说过 Swift 的目标之一是允许宏 检测其输入中的错误 并发出自定义错误 因此 让我们修改宏的实现 以生成更清晰的错误信息: “@DictionaryStorage 只能应用于结构体” 这样开发者能更好地 了解哪里做错了 实现这一点的关键是 展开方法的参数 此前我们一直忽略了它们 对于不同的角色 具体的实际参数略有不同 但对于成员宏而言 有三个参数 第一个参数叫做“attribute” 其类型是 AttributeSyntax 这是开发者为使用该宏而编写的 实际的 DictionaryStorage 属性 第二个参数叫做“declaration” 它是遵守 “DeclGroupSyntax”的一种类型 DeclGroupSyntax 是结构体、枚举、 类、actor、协议 和扩展的节点都遵守的协议 因此 这个参数为我们提供了 开发者附加属性的声明 最后一个参数叫做 “context” 它是遵守 “MacroExpansionContext”的一种类型 当宏实现需要与编译器通信时 会使用上下文对象 它可以执行一些不同的操作 包括发出错误和警告 我们将使用 所有这三个参数来发出错误 让我们来看看如何操作 首先 我们需要检测问题所在 我们将通过检查“declaration” 参数的类型来做到这一点 每种声明都有不同的类型 所以如果它是一个结构体 它的类型将是“StructDeclSyntax” 如果它是一个枚举 类型就是 “EnumDeclSyntax” 依此类推 因此 我们将编写一个 guard-else 来调用“declaration”参数的“is”方法 并传递“StructDeclSyntax” 如果声明不是结构体 我们将最终进入“else”代码块 目前 我们将返回一个空数组 这样宏不会向项目 添加任何代码 但我们真正想做的 是发出一个错误 现在 简单的方法是 直接抛出一个普通的 Swift 错误 但这样做并不能 对输出结果进行很好的控制 所以 我将向你展示更复杂的方法 让你能够创建更复杂的错误 第一步是创建一个名为 “Diagnostic”的类型实例 这是一个编译器术语 就像医生通过观察断腿的 X 光片 来诊断骨折一样 编译器或宏查看你的错误代码的 语法树来诊断错误或警告 因此 我们把代表错误的 实例称为“诊断” 一个诊断至少包含两个信息 第一个信息是错误发生的语法节点 这样编译器就知道 要将标记哪一行为错误 在这里 我们想要指向用户编写的 DictionaryStorage 属性 幸运的是 该属性是由 传递给方法的 “attribute”参数提供的 第二个信息是 你希望编译器生成的实际消息 你可以通过创建自定义类型 然后传递其实例来提供此信息 让我们快速地看一下怎么做 “MyLibDiagnostic”类型定义了 该模块可以生成的所有诊断信息 我们选择使用枚举 并为每个诊断提供一个案例 但你也可以使用其他类型 这种类型类似于 可抛出的 Swift 错误 它遵守 “DiagnosticMessage”协议 并拥有一系列属性来提供诊断信息 其中最重要的是“severity”属性 它用来指定诊断是错误还是警告
然后是“message”属性 它生成实际的错误消息 以及“diagnosticID”属性 你应该使用插件的模块名称作为域 并使用某种唯一的字符串作为 ID 我选择对此枚举使用字符串原始值 但这样只是为了方便 有了错误信息后 你就可以创建诊断 然后你让上下文进行诊断 这样就完成了
这是一个非常基本的诊断 但如果你愿意 你可以让它变得更高级 例如 你可以将 Fix-It 添加到诊断中 Fix-It 会被 Xcode 的 Fix 按钮自动应用 你还可以添加高亮显示 并附加指向代码中 其他位置的注释 这样你可以为开发者提供 真正一流的错误诊断体验 但是 一旦你确保你的宏被正确应用 你仍然需要实际创建展开 SwiftSyntax 为此提供了 多种不同的工具 语法节点是不可变的 但它们有很多 API 可以创建新节点 或返回现有节点的修饰版本 SwiftSyntaxBuilder 库会添加 SwiftUI 风格的语法构建器 其中一些子节点由尾随闭包指定 例如 多维数组宏可以使用语法构建器 为其创建的类型 生成适当数量的参数 我们用于创建 DictionaryStorage 属性 和初始化定式的 字符串字面量功能也支持插值
这些功能在不同情况下都非常有用 开发者可能会需要 将几个功能组合在一起 特别是在处理复杂的宏时 但是字符串字面量功能尤其适用于 生成大量代码的语法树 关于其插值功能 还有一些知识需要了解 让我们看看如何 使用它们来生成代码 之前 我们谈到了“unwrap”宏 它采用一个可选值和 一个消息字符串 并展开为包装在 闭包中的“guard let” 该代码的大致结构总是相同的 但许多内容都是 根据其具体的使用位置定制的 让我们重点关注 “guard let”语句 看看要如何 编写一个函数来生成该语句 首先 我们就用刚才看到的代码示例 并将其放入名为 “makeGuardStatement”的辅助方法中 该方法会返回语句语法节点 然后 我们将慢慢添加插值 来替换所有 需要根据其使用位置而不同的内容 我们要做的第一件事 是添加正确的消息字符串 消息字符串是任意表达式 因此我们将其作为 ExprSyntax 节点传入 然后将其插入 像这样的普通插值 可以向代码添加语法节点 但不能添加纯字符串 这是一个安全功能 可防止开发者意外插入无效代码 guard let 条件与此类似 不过它只是一个变量名 所以它是一个 token 而不是一个表达式 没关系 我们添加一个 TokenSyntax 参数并将其插入 就像我们插入表达式一样 当你将解包的表达式 添加到错误消息中时 会出现一种更棘手的情况 Swift 宏的特点之一是当它失败时 它会打印出你试图解包的代码 这意味着我们需要 创建一个字符串字面量 其中要包含语法节点的 字符串化版本
首先 我们将前缀从 Statement Syntax 字面量中提取出来 并将其放入一个纯字符串变量中 接下来我们插入该字符串 但我们会使用 以“literal:”开头的特殊插值 当你执行此操作时 SwiftSyntax 会将字符串的内容 添加为字符串字面量 这种方法也适用于 从宏计算的其他类型信息中 创建字面量 比如数字、Boolean、数组、字典 甚至可选项 现在我们在一个变量中建立了字符串 我们可以修改它 让它在消息中使用正确的代码 我们只需 为原始表达式添加一个参数 并将其“description”属性 插入到字符串中即可 你不需要做任何特殊的转义操作 “literal:”插值将自动检测 字符串是否包含特殊字符 并添加转义符 或切换到原始字面量 以确保代码有效 因此“literal:”插值让我们能 更轻松地做出正确操作 最后要处理的是文件和行号 这部分有点棘手 因为编译器 没有告诉宏它要展开到的源位置 然而 宏展开上下文 有一个可以用来生成 特殊语法节点的 API 编译器会将其转换为 带有源位置信息的字面量 让我们来看看怎么操作 我们为宏展开上下文 添加另一个参数 然后我们使用它的 “location of”方法 这会返回一个对象 该对象可以 为你提供的任何节点的位置 生成语法节点 如果该节点是你的宏创建的 而不是编译器 传递给你的节点 它将返回 nil 但我们知道“originalWrapped” 是用户编写的参数之一 因此它的位置永远不会为 nil 我们可以安全地强制解包结果 现在 你只需要插入文件和行号的 语法节点 然后就完成了 现在我们生成了 正确的“guard”语句 目前为止 我们只讨论了 如何让宏可以工作 那么接下来我们来聊聊 怎么让它们更好地工作 让我们从名称冲突开始 在之前的“unwrap”宏示例中 我们看到了如何 解包一个简单的变量名 但是 如果我们尝试 解包一个更复杂的表达式 宏的展开方式就会有所不同 它会生成代码 将表达式的结果 捕获到一个叫“wrappedValue”的 变量中 然后对其进行展开
但如果你尝试在消息中使用 名为“wrappedValue”的变量 会发生什么呢? 当编译器去寻找“wrappedValue”时 它会找到最近的一个 因此它会使用最近的 而不是你实际想要的那个
你可以试着通过选择一个你认为 用户不太可能意外使用的名称 来解决此问题 但是彻底杜绝这种情况发生 不是会更好吗?
这就是宏展开上下文中的 “makeUniqueName”方法的作用 它会返回一个在用户代码 或任何其他宏展开中 都绝对不会使用的变量名 这样你可以确保 消息字符串不会意外引用它 你可能会想 为什么 Swift 不自动阻止 这种情况发生呢? 某些语言具有 所谓的“卫生”宏系统 也就是宏内部的名称 与外部的名称是不同的 这样它们就不会发生冲突
Swift 不是这样的 因为我们发现很多宏 本身需要使用外部的名称 比如 DictionaryStorage 宏 它在类型上使用了 一个“dictionary”的属性 如果宏内部的 “dictionary”与外部的 “dictionary”意味着不同的东西 那么宏就很难正常工作了
有时候 你甚至需要 引入一个全新的名称 以供非宏代码访问 对等宏、成员宏 和声明宏基本上完全是 为此而存在的 但当它们存在时 它们需要声明 它们要添加的名称 以便编译器知道它们的存在 并且它们要在角色属性内 执行此操作 你或许之前没有注意到 但实际上我们一直在看到这些声明 DictionaryStorage 宏上的 “member”角色 有一个“names:”参数 指定了 “dictionary”和“init” 这两个名称 实际上 本次讲座涉及的 大多数宏中 都至少有一个角色 带有“names”参数
你可以使用五种名称限定符: “overloaded”表示宏会添加 与它所附加的内容 具有完全相同的基本名称的声明; “prefixed”表示宏会添加 具有相同基本名称的声明 但会添加指定的前缀; “suffixed”也是一样 但它会添加后缀而不是前缀; “named”表示宏添加的声明 具有特定的、固定的基本名称; 以及“arbitrary” 表示宏添加的声明 有其他名称 但这些名称 无法使用这些规则来描述 使用“arbitrary”的 情况非常普遍 例如 我们的多维数组宏 会声明一种类型 其名称是根据 其参数之一计算得出的 因此需要指定“arbitrary” 但如果你可以使用其他名称限定符 请使用其他限定符 这样可以让编译器 和代码完成等工具的速度变得更快 讲座进行到现在 我相信你已经迫不及待地 想编写自己的第一个宏了 而且你可能已经有了一个好主意: 先编写一个在展开时 插入日期和时间的宏就好了 这主意真不错 不是吗? 错了 事实证明 你不能编写这个宏 让我来解释一下为什么 宏只能使用 编译器提供给它们的信息 编译器假设宏的实现是纯函数 并且如果它提供的数据没有改变 那么展开的结果也不能改变 如果你绕过这个规则 可能会导致不一致的行为 宏系统的设计旨在防止一些 可能违反此规则的行为 编译器插件会在沙盒中运行 沙盒可以阻止 宏实现读取磁盘上的文件 或访问网络 但是沙盒并不能阻止所有不良行为 你可以使用 API 获取日期 或随机数等信息 或者将一个展开中的信息 保存在全局变量中 并在另一个展开中使用它 但是 如果你这样做 你的宏可能会表现异常 所以请不要这样做 最后 让我们来谈谈同样很重要的测试 你的宏插件只是一个 普通的 Swift 模块 这意味着你可以 而且绝对应该 为其编写正常的单元测试 测试驱动的开发是一种非常有效的 开发 Swift 宏的方法 SwiftSyntaxMacrosTestSupport 中的 “assertMacroExpansion”辅助 可以检查宏是否正确展开 你只需给它一个宏的示例 以及它应该展开成的代码 它就会确保它们匹配 我们今天学到了 关于 Swift 宏的很多知识 宏可以让你设计新的语言特性 将一个小的使用场景 “展开”为一段更复杂的代码 从而减少样板代码 你通常在库中 与其他 API 一起声明宏 但你实际上 是在一个单独的插件中实现宏 该插件会在一个安全的沙盒中 运行 Swift 代码 宏的角色会告诉你 应在何处使用它 以及其展开如何 集成到程序的其余部分中 你可以 而且应该为你的宏 编写单元测试 以确保它们按预期工作 如果你还没有看过 “编写 Swift 宏”讲座 接下来你应该查看它 该讲座介绍了如何使用 Xcode 的宏开发工具 和宏包模板 以及如何检查 SwiftSyntax 树 并从中提取信息 还有如何围绕单元测试 构建宏开发工作流程 感谢你的观看 祝你编程愉快 ♪ ♪
-
-
0:44 - The #unwrap expression macro, with a more complicated argument
let image = #unwrap(request.downloadedImage, message: "was already checked") // Begin expansion for "#unwrap" { [wrappedValue = request.downloadedImage] in guard let wrappedValue else { preconditionFailure( "Unexpectedly found nil: ‘request.downloadedImage’ " + "was already checked", file: "main/ImageLoader.swift", line: 42 ) } return wrappedValue }() // End expansion for "#unwrap"
-
0:50 - Existing features using expansions (1)
struct Smoothie: Codable { var id, title, description: String var measuredIngredients: [MeasuredIngredient] static let berryBlue = Smoothie(id: "berry-blue", title: "Berry Blue") { """ Filling and refreshing, this smoothie \ will fill you with joy! """ Ingredient.orange .measured(with: .cups).scaled(by: 1.5) Ingredient.blueberry .measured(with: .cups) Ingredient.avocado .measured(with: .cups).scaled(by: 0.2) } }
-
1:11 - Existing features using expansions (2)
struct Smoothie: Codable { var id, title, description: String var measuredIngredients: [MeasuredIngredient] // Begin expansion for Codable private enum CodingKeys: String, CodingKey { case id, title, description, measuredIngredients } init(from decoder: Decoder) throws { … } func encode(to encoder Encoder) throws { … } // End expansion for Codable static let berryBlue = Smoothie(id: "berry-blue", title: "Berry Blue") { """ Filling and refreshing, this smoothie \ will fill you with joy! """ Ingredient.orange .measured(with: .cups).scaled(by: 1.5) Ingredient.blueberry .measured(with: .cups) Ingredient.avocado .measured(with: .cups).scaled(by: 0.2) } }
-
3:16 - Macros inputs are complete, type-checked, and validated
#unwrap(1 + ) // error: expected expression after operator @AddCompletionHandler(parameterName: 42) // error: cannot convert argument of type 'Int' to expected type 'String' func sendRequest() async throws -> Response @DictionaryStorage class Options { … } // error: '@DictionaryStorage' can only be applied to a 'struct'
-
3:45 - Macro expansions are inserted in predictable ways
func doThingy() { startDoingThingy() #someUnknownMacro() finishDoingThingy() }
-
4:51 - How macros work, featuring #stringify
func printAdd(_ a: Int, _ b: Int) { let (result, str) = #stringify(a + b) // Begin expansion for "#stringify" (a + b, "a + b") // End expansion for "#stringify" print("\(str) = \(result)") } printAdd(1, 2) // prints "a + b = 3"
-
5:43 - Macro declaration for #stringify
/// Creates a tuple containing both the result of `expr` and its source code represented as a /// `String`. @freestanding(expression) macro stringify<T>(_ expr: T) -> (T, String)
-
7:11 - What’s an expression?
let numPixels = (x + width) * (y + height) // ^~~~~~~~~~~~~~~~~~~~~~~~~~ This is an expression // ^~~~~~~~~ But so is this // ^~~~~ And this
-
7:34 - The #unwrap expression macro: motivation
// Some teams are nervous about this: let image = downloadedImage! // Alternatives are super wordy: guard let image = downloadedImage else { preconditionFailure("Unexpectedly found nil: downloadedImage was already checked") }
-
8:03 - The #unwrap expression macro: macro declaration
/// Force-unwraps the optional value passed to `expr`. /// - Parameter message: Failure message, followed by `expr` in single quotes @freestanding(expression) macro unwrap<Wrapped>(_ expr: Wrapped?, message: String) -> Wrapped
-
8:21 - The #unwrap expression macro: usage
let image = #unwrap(downloadedImage, message: "was already checked") // Begin expansion for "#unwrap" { [downloadedImage] in guard let downloadedImage else { preconditionFailure( "Unexpectedly found nil: ‘downloadedImage’ " + "was already checked", file: "main/ImageLoader.swift", line: 42 ) } return downloadedImage }() // End expansion for "#unwrap"
-
9:09 - The #makeArrayND declaration macro: motivation
public struct Array2D<Element>: Collection { public struct Index: Hashable, Comparable { var storageIndex: Int } var storage: [Element] var width1: Int public func makeIndex(_ i0: Int, _ i1: Int) -> Index { Index(storageIndex: i0 * width1 + i1) } public subscript (_ i0: Int, _ i1: Int) -> Element { get { self[makeIndex(i0, i1)] } set { self[makeIndex(i0, i1)] = newValue } } public subscript (_ i: Index) -> Element { get { storage[i.storageIndex] } set { storage[i.storageIndex] = newValue } } // Note: Omitted additional members needed for 'Collection' conformance } public struct Array3D<Element>: Collection { public struct Index: Hashable, Comparable { var storageIndex: Int } var storage: [Element] var width1, width2: Int public func makeIndex(_ i0: Int, _ i1: Int, _ i2: Int) -> Index { Index(storageIndex: (i0 * width1 + i1) * width2 + i2) } public subscript (_ i0: Int, _ i1: Int, _ i2: Int) -> Element { get { self[makeIndex(i0, i1, i2)] } set { self[makeIndex(i0, i1, i2)] = newValue } } public subscript (_ i: Index) -> Element { get { storage[i.storageIndex] } set { storage[i.storageIndex] = newValue } } // Note: Omitted additional members needed for 'Collection' conformance }
-
10:03 - The #makeArrayND declaration macro: macro declaration
/// Declares an `n`-dimensional array type named `Array<n>D`. /// - Parameter n: The number of dimensions in the array. @freestanding(declaration, names: arbitrary) macro makeArrayND(n: Int)
-
10:15 - The #makeArrayND declaration macro: usage
#makeArrayND(n: 2) // Begin expansion for "#makeArrayND" public struct Array2D<Element>: Collection { public struct Index: Hashable, Comparable { var storageIndex: Int } var storage: [Element] var width1: Int public func makeIndex(_ i0: Int, _ i1: Int) -> Index { Index(storageIndex: i0 * width1 + i1) } public subscript (_ i0: Int, _ i1: Int) -> Element { get { self[makeIndex(i0, i1)] } set { self[makeIndex(i0, i1)] = newValue } } public subscript (_ i: Index) -> Element { get { storage[i.storageIndex] } set { storage[i.storageIndex] = newValue } } } // End expansion for "#makeArrayND" #makeArrayND(n: 3) #makeArrayND(n: 4) #makeArrayND(n: 5)
-
11:23 - The @AddCompletionHandler peer macro: motivation
/// Fetch the avatar for the user with `username`. func fetchAvatar(_ username: String) async -> Image? { ... } func fetchAvatar(_ username: String, onCompletion: @escaping (Image?) -> Void) { Task.detached { onCompletion(await fetchAvatar(username)) } }
-
11:51 - The @AddCompletionHandler peer macro: macro declaration
/// Overload an `async` function to add a variant that takes a completion handler closure as /// a parameter. @attached(peer, names: overloaded) macro AddCompletionHandler(parameterName: String = "completionHandler")
-
11:59 - The @AddCompletionHandler peer macro: usage
/// Fetch the avatar for the user with `username`. @AddCompletionHandler(parameterName: "onCompletion") func fetchAvatar(_ username: String) async -> Image? { ... } // Begin expansion for "@AddCompletionHandler" /// Fetch the avatar for the user with `username`. /// Equivalent to ``fetchAvatar(username:)`` with /// a completion handler. func fetchAvatar( _ username: String, onCompletion: @escaping (Image?) -> Void ) { Task.detached { onCompletion(await fetchAvatar(username)) } } // End expansion for "@AddCompletionHandler"
-
12:36 - The @DictionaryStorage accessor macro: motivation
struct Person: DictionaryRepresentable { init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] var name: String { get { dictionary["name"]! as! String } set { dictionary["name"] = newValue } } var height: Measurement<UnitLength> { get { dictionary["height"]! as! Measurement<UnitLength> } set { dictionary["height"] = newValue } } var birthDate: Date? { get { dictionary["birth_date"] as! Date? } set { dictionary["birth_date"] = newValue as Any? } } }
-
13:04 - The @DictionaryStorage accessor macro: declaration
/// Adds accessors to get and set the value of the specified property in a dictionary /// property called `storage`. @attached(accessor) macro DictionaryStorage(key: String? = nil)
-
13:20 - The @DictionaryStorage accessor macro: usage
struct Person: DictionaryRepresentable { init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] @DictionaryStorage var name: String // Begin expansion for "@DictionaryStorage" { get { dictionary["name"]! as! String } set { dictionary["name"] = newValue } } // End expansion for "@DictionaryStorage" @DictionaryStorage var height: Measurement<UnitLength> // Begin expansion for "@DictionaryStorage" { get { dictionary["height"]! as! Measurement<UnitLength> } set { dictionary["height"] = newValue } } // End expansion for "@DictionaryStorage" @DictionaryStorage(key: "birth_date") var birthDate: Date? // Begin expansion for "@DictionaryStorage" { get { dictionary["birth_date"] as! Date? } set { dictionary["birth_date"] = newValue as Any? } } // End expansion for "@DictionaryStorage" }
-
13:56 - The @DictionaryStorage member attribute macro: macro declaration
/// Adds accessors to get and set the value of the specified property in a dictionary /// property called `storage`. @attached(memberAttribute) @attached(accessor) macro DictionaryStorage(key: String? = nil)
-
14:46 - The @DictionaryStorage member attribute macro: usage
@DictionaryStorage struct Person: DictionaryRepresentable { init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] // Begin expansion for "@DictionaryStorage" @DictionaryStorage // End expansion for "@DictionaryStorage" var name: String // Begin expansion for "@DictionaryStorage" @DictionaryStorage // End expansion for "@DictionaryStorage" var height: Measurement<UnitLength> @DictionaryStorage(key: "birth_date") var birthDate: Date? }
-
15:52 - The @DictionaryStorage member macro: macro definition
/// Adds accessors to get and set the value of the specified property in a dictionary /// property called `storage`. @attached(member, names: named(dictionary), named(init(dictionary:))) @attached(memberAttribute) @attached(accessor) macro DictionaryStorage(key: String? = nil)
-
16:26 - The @DictionaryStorage member macro: usage
// The @DictionaryStorage member macro @DictionaryStorage struct Person: DictionaryRepresentable { // Begin expansion for "@DictionaryStorage" init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] // End expansion for "@DictionaryStorage" var name: String var height: Measurement<UnitLength> @DictionaryStorage(key: "birth_date") var birthDate: Date? }
-
16:59 - The @DictionaryStorage conformance macro: macro definition
/// Adds accessors to get and set the value of the specified property in a dictionary /// property called `storage`. @attached(conformance) @attached(member, names: named(dictionary), named(init(dictionary:))) @attached(memberAttribute) @attached(accessor) macro DictionaryStorage(key: String? = nil)
-
17:09 - The @DictionaryStorage conformance macro: usage
struct Person // Begin expansion for "@DictionaryStorage" : DictionaryRepresentable // End expansion for "@DictionaryStorage" { var name: String var height: Measurement<UnitLength> @DictionaryStorage(key: "birth_date") var birthDate: Date? }
-
17:28 - @DictionaryStorage starting point
struct Person: DictionaryRepresentable { init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] var name: String { get { dictionary["name"]! as! String } set { dictionary["name"] = newValue } } var height: Measurement<UnitLength> { get { dictionary["height"]! as! Measurement<UnitLength> } set { dictionary["height"] = newValue } } var birthDate: Date? { get { dictionary["birth_date"] as! Date? } set { dictionary["birth_date"] = newValue as Any? } } }
-
17:32 - @DictionaryStorage ending point
@DictionaryStorage struct Person // Begin expansion for "@DictionaryStorage" : DictionaryRepresentable // End expansion for "@DictionaryStorage" { // Begin expansion for "@DictionaryStorage" init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] // End expansion for "@DictionaryStorage" // Begin expansion for "@DictionaryStorage" @DictionaryStorage // End expansion for "@DictionaryStorage" var name: String // Begin expansion for "@DictionaryStorage" { get { dictionary["name"]! as! String } set { dictionary["name"] = newValue } } // End expansion for "@DictionaryStorage" // Begin expansion for "@DictionaryStorage" @DictionaryStorage // End expansion for "@DictionaryStorage" var height: Measurement<UnitLength> // Begin expansion for "@DictionaryStorage" { get { dictionary["height"]! as! Measurement<UnitLength> } set { dictionary["height"] = newValue } } // End expansion for "@DictionaryStorage" @DictionaryStorage(key: "birth_date") var birthDate: Date? // Begin expansion for "@DictionaryStorage" { get { dictionary["birth_date"] as! Date? } set { dictionary["birth_date"] = newValue as Any? } } // End expansion for "@DictionaryStorage" }
-
17:35 - @DictionaryStorage ending point (without expansions)
@DictionaryStorage struct Person { var name: String var height: Measurement<UnitLength> @DictionaryStorage(key: "birth_date") var birthDate: Date? }
-
18:01 - Macro implementations
/// Creates a tuple containing both the result of `expr` and its source code represented as a /// `String`. @freestanding(expression) macro stringify<T>(_ expr: T) -> (T, String) = #externalMacro( module: "MyLibMacros", type: "StringifyMacro" )
-
19:18 - Implementing @DictionaryStorage’s @attached(member) role (1)
import SwiftSyntax import SwiftSyntaxMacros import SwiftSyntaxBuilder struct DictionaryStorageMacro: MemberMacro { static func expansion( of attribute: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { return [ "init(dictionary: [String: Any]) { self.dictionary = dictionary }", "var dictionary: [String: Any]" ] } }
-
19:52 - Code used to demonstrate SwiftSyntax trees
@DictionaryStorage struct Person { var name: String var height: Measurement<UnitLength> @DictionaryStorage(key: "birth_date") var birthDate: Date? }
-
22:00 - Implementing @DictionaryStorage’s @attached(member) role (2)
import SwiftSyntax import SwiftSyntaxMacros import SwiftSyntaxBuilder struct DictionaryStorageMacro: MemberMacro { static func expansion( of attribute: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { return [ "init(dictionary: [String: Any]) { self.dictionary = dictionary }", "var dictionary: [String: Any]" ] } }
-
24:29 - A type that @DictionaryStorage isn’t compatible with
@DictionaryStorage enum Gender { case other(String) case female case male // Begin expansion for "@DictionaryStorage" init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] // End expansion for "@DictionaryStorage" }
-
25:17 - Expansion method with error checking
import SwiftSyntax import SwiftSyntaxMacros import SwiftSyntaxBuilder struct DictionaryStorageMacro: MemberMacro { static func expansion( of attribute: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { guard declaration.is(StructDeclSyntax.self) else { let structError = Diagnostic( node: attribute, message: MyLibDiagnostic.notAStruct ) context.diagnose(structError) return [] } return [ "init(dictionary: [String: Any]) { self.dictionary = dictionary }", "var dictionary: [String: Any]" ] } } enum MyLibDiagnostic: String, DiagnosticMessage { case notAStruct var severity: DiagnosticSeverity { return .error } var message: String { switch self { case .notAStruct: return "'@DictionaryStorage' can only be applied to a 'struct'" } } var diagnosticID: MessageID { MessageID(domain: "MyLibMacros", id: rawValue) } }
-
29:32 - Parameter list for `ArrayND.makeIndex`
FunctionParameterListSyntax { for dimension in 0 ..< numDimensions { FunctionParameterSyntax( firstName: .wildcardToken(), secondName: .identifier("i\(dimension)"), type: TypeSyntax("Int") ) } }
-
30:17 - The #unwrap expression macro: revisited
let image = #unwrap(downloadedImage, message: "was already checked") // Begin expansion for "#unwrap" { [downloadedImage] in guard let downloadedImage else { preconditionFailure( "Unexpectedly found nil: ‘downloadedImage’ " + "was already checked", file: "main/ImageLoader.swift", line: 42 ) } return downloadedImage }() // End expansion for "#unwrap"
-
30:38 - Implementing the #unwrap expression macro: start
static func makeGuardStmt() -> StmtSyntax { return """ guard let downloadedImage else { preconditionFailure( "Unexpectedly found nil: ‘downloadedImage’ " + "was already checked", file: "main/ImageLoader.swift", line: 42 ) } """ }
-
30:57 - Implementing the #unwrap expression macro: the message string
static func makeGuardStmt(message: ExprSyntax) -> StmtSyntax { return """ guard let downloadedImage else { preconditionFailure( "Unexpectedly found nil: ‘downloadedImage’ " + \(message), file: "main/ImageLoader.swift", line: 42 ) } """ }
-
31:21 - Implementing the #unwrap expression macro: the variable name
static func makeGuardStmt(wrapped: TokenSyntax, message: ExprSyntax) -> StmtSyntax { return """ guard let \(wrapped) else { preconditionFailure( "Unexpectedly found nil: ‘downloadedImage’ " + \(message), file: "main/ImageLoader.swift", line: 42 ) } """ }
-
31:44 - Implementing the #unwrap expression macro: interpolating a string as a literal
static func makeGuardStmt(wrapped: TokenSyntax, message: ExprSyntax) -> StmtSyntax { let messagePrefix = "Unexpectedly found nil: ‘downloadedImage’ " return """ guard let \(wrapped) else { preconditionFailure( \(literal: messagePrefix) + \(message), file: "main/ImageLoader.swift", line: 42 ) } """ }
-
32:11 - Implementing the #unwrap expression macro: adding an expression as a string
static func makeGuardStmt(wrapped: TokenSyntax, originalWrapped: ExprSyntax, message: ExprSyntax) -> StmtSyntax { let messagePrefix = "Unexpectedly found nil: ‘\(originalWrapped.description)’ " return """ guard let \(wrapped) else { preconditionFailure( \(literal: messagePrefix) + \(message), file: "main/ImageLoader.swift", line: 42 ) } """ }
-
33:00 - Implementing the #unwrap expression macro: inserting the file and line numbers
static func makeGuardStmt(wrapped: TokenSyntax, originalWrapped: ExprSyntax, message: ExprSyntax, in context: some MacroExpansionContext) -> StmtSyntax { let messagePrefix = "Unexpectedly found nil: ‘\(originalWrapped.description)’ " let originalLoc = context.location(of: originalWrapped)! return """ guard let \(wrapped) else { preconditionFailure( \(literal: messagePrefix) + \(message), file: \(originalLoc.file), line: \(originalLoc.line) ) } """ }
-
34:05 - The #unwrap expression macro, with a name conflict
let wrappedValue = "🎁" let image = #unwrap(request.downloadedImage, message: "was \(wrappedValue)") // Begin expansion for "#unwrap" { [wrappedValue = request.downloadedImage] in guard let wrappedValue else { preconditionFailure( "Unexpectedly found nil: ‘request.downloadedImage’ " + "was \(wrappedValue)", file: "main/ImageLoader.swift", line: 42 ) } return wrappedValue }() // End expansion for "#unwrap"
-
34:30 - The MacroExpansion.makeUniqueName() method
let captureVar = context.makeUniqueName() return """ { [\(captureVar) = \(originalWrapped)] in \(makeGuardStmt(wrapped: captureVar, …)) \(makeReturnStmt(wrapped: captureVar)) } """
-
35:44 - Declaring a macro’s names
@attached(conformance) @attached(member, names: named(dictionary), named(init(dictionary:))) @attached(memberAttribute) @attached(accessor) macro DictionaryStorage(key: String? = nil) @attached(peer, names: overloaded) macro AddCompletionHandler(parameterName: String = "completionHandler") @freestanding(declaration, names: arbitrary) macro makeArrayND(n: Int)
-
38:28 - Macros are testable
import MyLibMacros import XCTest import SwiftSyntaxMacrosTestSupport final class MyLibTests: XCTestCase { func testMacro() { assertMacroExpansion( """ @DictionaryStorage var name: String """, expandedSource: """ var name: String { get { dictionary["name"]! as! String } set { dictionary["name"] = newValue } } """, macros: ["DictionaryStorage": DictionaryStorageMacro.self]) } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。