大多数浏览器和
Developer App 均支持流媒体播放。
-
编写 Swift 宏
了解如何使用 Swift 宏让你的代码库更具表现力和易读性。和我们一起写代码,探索宏如何帮助你避免编写重复的代码,并了解如何在你的 App 中使用它们。我们将分享宏的构建模块,向你展示如何对其进行测试,并带你了解如何从宏中发出编译错误。
章节
- 1:15 - Overview
- 5:10 - Create a macro using Xcode's macro template
- 10:50 - Macro roles
- 11:40 - Write a SlopeSubset macro to define an enum subset
- 20:17 - Inspect the syntax tree structure in the debugger
- 24:35 - Add a macro to an Xcode project
- 27:05 - Emit error messages from a macro
- 30:12 - Generalize SlopeSubset to a generic EnumSubset macro
资源
相关视频
WWDC23
-
下载
♪ ♪
Alex Hoppen:谁喜欢 写重复的样板代码呢? 没有人喜欢! 所以我们在 Swift 5.9 中 引入了 Swift 宏 Swift 宏可以让开发者 在编译时生成重复代码 让你的 App 的代码库 更具表现力 更容易阅读 我叫 Alex Hoppen 今天我要向你展示 如何编写自己的宏 首先 我会简要介绍宏的工作原理
之后 我们将直接进入 Xcode 看看如何创建你的第一个宏
在 Xcode 中完成了 我们的第一个宏之后 我们将了解更多可使用的宏角色 并且我将向你展示如何使用宏 来简化我当前正在开发的 App 的代码库
最后 我将介绍 当宏在特定背景中不适用时 它们将如何向编译器 传达错误或警告信息
让我们开始吧 在这里 我们有一个 一年级学生可以用来 练习算数的的计算列表 我们将结果作为整数 显示在元组的左侧 并将计算作为字符串字面量 显示在元组的右侧 但是这种方式重复、冗余 甚至容易出错 因为没有人能够保证 结果与计算实际匹配 好消息是 在 Swift 5.9 中 我们可以 定义一个 stringify 宏来简化这个过程
这个宏恰好也是包含在 Xcode 模板中的宏 stringify 宏 只接受计算这一个参数 在编译时 它会展开成 我们之前看到的元组 以确保计算和结果匹配 那么这是怎么做到的呢? 让我们来单独看一下这个宏的定义
它看起来很像一个函数 stringify 宏接受 一个整数作为输入参数 并输出一个包含结果、 整数和计算结果的元组 构成一个字符串 如果宏表达式的实际参数 与宏的参数不匹配 或者本身没有进行类型检查 则编译器将发出错误 而不应用宏展开
例如 如果我将一个 字符串字面量传递给这个宏 编译器会警告“String”不能转换为 预期的参数类型“Int” 这与 C 语言的宏不同 后者会在类型检查之前的 预处理器阶段进行评估 而这个宏允许我们使用 开发者熟悉和喜爱的 Swift 函数的所有功能 比如能够让宏变为泛型
还要注意的是 这个宏是使用 独立的表达式宏角色声明的 这意味着开发者可以在任何 可以使用表达式的地方使用宏 并且它将以井号字符标示 就像我们看到的 #stringify 另外一种宏 是可以增强声明的附加宏 我稍后会详细介绍这种宏 在检查所有实际参数 与宏的参数匹配之后 编译器就会执行宏展开 为了了解其工作原理 让我们重点关注一个单独的宏表达式
为了执行展开 每个宏都会在 编译器插件中定义其实现 编译器会将整个宏表达式的源代码 发送到该插件 宏插件要做的第一件事 就是将宏的源代码 解析为 SwiftSyntax 树 这个树是宏的源代码 精确的结构表示 它将成为宏运行的基础
例如 我们的 “stringify”宏在树中 表示为宏展开表达式节点 该表达式的宏名称为 “stringify” 并且它只接受一个参数 即中缀运算符加号 应用在 2 和 3 之间 Swift 宏的真正强大之处在于 宏的实现本身就是 一个用 Swift 编写的程序 可以对语法树进行任何转换
在本例中 它生成了我们之前看到的元组 然后 它将再次把 生成的语法树序列化为源代码 并将其发送给编译器 编译器将用展开的代码 替换宏表达式
这真的太酷了 但我现在想知道它们 在代码中实际是什么样子 Xcode 中新的宏模板 定义了 我们刚才看到的 stringify 宏 让我们来看看这个模板 探讨一下这个宏的定义、 展开方式以及如何测试宏 为了创建模板 我点按“File” 选择“New” 然后是“Package” 然后我选择“Swift Macro”模板
让我们将第一个宏命名为“WWDC”
那么模板中有什么呢? 这里有 #stringify 宏的调用 与之前看到的宏类似 它接受一个“a + b”参数 并返回结果 以及生成它的代码 如果我想知道宏展开后的内容 可以右键点按它 并选择 Expand Macro
这跟我们之前看到的完全一样 但这个宏是如何定义的呢? 让我们来看它的定义
这里我们有一个 之前的“stringify”宏 稍微泛化的版本 这个宏不只接受一个整数 而是泛型的 可以接收任何类型 T
它被声明为外部宏
这告诉编译器 在执行展开时 它需要查看 WWDCMacros 模块中的 StringifyMacro 类型
这个类型是如何定义的呢?
让我们来仔细看看 因为 stringify 被声明为 一个独立的表达式宏 所以 StringifyMacro 类型 需要符合 ExpressionMacro 协议
这个协议只有一个要求: 展开函数 它采用宏表达式本身的语法树 以及可用于与编译器通信的上下文
然后展开函数 返回重写的表达式语法
那么它在实现中都做什么呢? 首先 它检索宏表达式的单个参数 它知道这个参数存在 因为 stringify 被声明为 接受单个参数 并且在宏展开之前 所有参数都需要进行类型检查 然后 它使用字符串 插值来创建元组的语法树 第一个元素是参数本身 第二个元素是一个包含 参数源代码的字符串字面量
注意 这个函数在这里 没有返回字符串 而是返回了一个表达式语法 这个宏 将自动调用 Swift 解析器 将这个字面量转换成语法树 而且由于它在第二个参数中 使用了字面量插值的样式 它会确保字面量内容被正确转义 没有人喜欢错误 但我更不喜欢的 是那种在代码中看不到的错误 除非我明确通过 展开宏才能看到它们 所以开发者需要 确保自己的宏经过充分测试 因为宏没有副作用 而且语法树的源代码易于比较 所以测试宏的一个好办法 是编写单元测试 宏模板中就自带了一个单元测试
这个测试用例使用 SwiftSyntax 包中的 “assertMacroExpansion”函数 来验证“stringify”宏 是否正确展开
它将我们之前看到的 “#stringify(a + b)” 表达式作为输入 并断言宏展开后 它会生成一个包含“a + b” 和字符串字面量 “a + b”的元组
为了告诉测试用例如何展开宏 它传递了“testMacros”参数 该参数指定宏“#stringify” 应使用“StringifyMacro” 类型进行展开 让我们来运行一下测试 看看测试是否通过 你可能已经用同样的方式 为你的 App 进行了测试
测试通过了 这样我们就有了我们的第一个宏
我们在其中看到了 它的基本编译分块 宏声明定义了宏的签名 还声明了宏角色 编译器插件执行展开操作 它本身是一个 用 Swift 编写的程序 并且在 SwiftSyntax 树上运行
我们还看到宏非常容易进行测试 因为它们是语法树的确定性转换 而且语法树的源代码很容易比较 那么你可能会想: “在哪些情况下还可以使用宏呢?” 我们已经了解了 freestanding (expression) 宏 简单回顾一下 这种宏是以井号开头的 并允许开发者重写整个宏表达式 还有一个独立的声明角色 它会展开成一个声明 而不是一个表达式 其他类型的宏是附加宏 它们以 @ 符号开头 与属性类似 并允许宏增强它们所附加的声明 例如 @attached (member) 宏 会添加所附加类型的新 member 要进一步了解关于这些 其他角色的信息 我强烈推荐你查看 “深入了解 Swift 宏” Becca 会在这个讲座中 详细介绍它们 但我想重点介绍一下 @attached (member) 的作用 因为它们可以帮助我优化 我正在开发的 App 的代码库 我还是一名滑雪教练 我最近在开发一个 App 用来帮我规划 应该带我的学生去哪里滑雪
作为滑雪教练 你绝对要避免的一件事 就是带初学者去 对他们来说太难的坡道 我想通过 Swift 类型系统 来保证这一点 因此 除了包含我最爱的滑雪胜地的 所有坡道的 Slope 枚举之外 我还有一个 EasySlope 类型 只包含适合初学者的坡道 它包含一个初始化定式 用于在坡道适合初学者的时候 将其转换为简单坡道; 它也包含一个计算属性 用于将简单坡道转化为一般坡道
虽然这样提供了很好的类型安全性 但却非常重复 如果我想添加一个简单的坡道 我需要将它添加到 Slope……
然后是 EasySlope、 初始化定式 以及计算属性 我们来看看是否可以用宏 来改进这些情况 我们想要做的是自动生成初始化定式 和计算属性 我们该如何做到这一点呢? 初始化定式和计算属性 都是 EasySlope 类型的 member 所以我们需要声明 一个 @attached (member) 宏
接下来 我们将创建 包含宏实现的编译器插件
为了确保我们的宏行为符合预期 我们希望 以测试驱动的方式进行开发 因此 在编写测试用例之前 我们将保持其实现为空
在我们定义了宏在 测试用例中的行为之后 我们将编写相应的 实现代码以匹配该测试用例
最后 我们将把这个新宏 整合到我的 App 中 如果一切顺利 我们就可以移除初始化定式 让宏为我们生成它
为了开发宏 我们使用之前创建的模板 由于我的 App 实际上 不需要“#stringify”宏 我已经把它移除了 我通过使用 “@attached (member)”属性 来声明一个新附加的 member 宏
我将其命名为 SlopeSubset 因为 EasySlope 是 Slope 的子集
该宏还定义了 它引入的 member 的名称
在这个演示中 我将向你展示如何生成初始化定式 生成计算属性的步骤也非常相似 因为它也只是一个 switch 语句 用于切换所有情况 通过这个声明 我们定义了宏 但我们还没有实现 它实际执行的展开
为此 我们的宏引用了 WWDCMacros 模块中的 SlopeSubsetMacro 类型 让我们来创建这个类型 这样我们就可以继续 真正令人兴奋的部分: 实际的宏实现
由于我们将 SlopeSubset 声明为附加 member 宏 相应的实现需要符合 MemberMacro 协议
该协议只有一个要求: “expansion”函数 和 ExpressionMacro 相似
“expansion”函数接受我们将宏 应用于声明的属性 以及正在应用宏的声明作为参数 在我们的例子中 就是 EasySlope 枚举声明
然后 该宏返回 它想添加到该声明中的 所有新 member 的列表
我知道直接开始 实现这个转换非常诱人 但我们已经说好了 要先为它编写一个测试用例
所以现在让我们只返回一个空数组 表示不应添加任何新 member
最后 我们需要让 SlopeSubset 在编译器中可见 为此 我将它添加到这里的 “providingMacros” 属性中
在继续深入之前 我想先确保 我们目前的东西都可以运行 虽然我可以尝试 在 Xcode 中应用宏 并查看展开的代码 但我更喜欢为它编写一个测试用例 这样每当我对宏进行更改时 都可以重新运行它 而不用引入回归测试
就像在模板中的测试用例中一样 我们使用“assertMacroExpansion” 函数来验证宏的行为
我们想要测试的是宏在应用于 EasySlope 类型时生成的内容 所以我们将其作为测试用例的输入
由于宏目前还没有执行任何操作 我们只是期望它删除属性 而不添加任何新 member 因此预期的展开代码与输入相同 只是没有“@SlopeSubset”
最后 我们需要让测试用例知道 它应该使用 SlopeSubsetMacro 实现 来扩展 SlopeSubset 宏 为此 我们需要在 “testMacros”字典中将宏名称 映射到它的实现类型 并将该字典传递给断言函数
现在让我们运行测试 来检查我们到目前 所写的东西是否真的有效
它确实有效 太棒了 但我们真正想要的是检查我们的宏 是否真的生成了初始化定式 而不仅仅是删除属性 因此 我会将之前手动 编写的代码复制到测试用例中 因为这其实才是 我们希望插件生成的内容
如果我们再次运行测试……
它失败了 因为我们的宏实际上 还没有生成初始化定式
让我们更改这一点
初始化定式会切换 EasySlopes 枚举中声明的所有枚举元素 所以我们需要做的第一件事 是从声明中检索这些枚举元素 由于枚举元素 只能在枚举声明内部声明 我们首先将“declaration” 强制转换为枚举声明
如果宏被附加到 一个不是枚举的类型上 我们应该发出一个错误 我添加了一个 TODO 以便提醒我们之后处理它 并且暂时返回一个空数组 接下来 我们需要 获取枚举声明的所有元素 为了弄清楚如何做到这一点 我想检查 SwiftSyntax 树中 我们枚举的语法结构
由于宏的实现只是 一个普通的 Swift 程序 我可以使用开发者在 Xcode 中 熟悉的所有工具来调试程序 例如 我可以在 展开函数内设置一个断点 并运行测试用例以触发该断点
现在 我们将调试器 暂停在宏的实现中 “enumDecl”是 EasySlopes 的枚举 我们可以通过输入“po enumDecl” 在调试器中打印它
让我们检查一下输出
语法树的最里面的节点 代表枚举元素 即“beginnersParadise” 和“practiceRun”坡道 为了获取它们 我们需要按照 语法树中给出的结构进行操作 让我们逐步浏览该结构 并在过程中编写访问代码
枚举声明有一个名为 “memberBlock”的子级 该 memberBlock 包含左右括号 和实际的 member 因此 要访问 member 我们先从 “enumDecl.memberBlock.members”开始
这些 member 包含实际的声明 以及可选的分号 我们想要关注的是这些声明 特别是 那些实际声明枚举案例的声明 我正在使用 compactMap 来获取所有是枚举案例的 member 声明列表 每个案例声明可以声明多个元素 这是因为我不需要 在单独的案例关键字后 在新的一行中声明每个坡道 我可以把它们写在同一行中 如“case beginnersParadise, practiceRun”
要检索所有元素 我们可以使用“flatMap”
现在 我们已经 检索到了所有的元素 可以开始构造我们实际想要 添加到 EasySlope 的初始化定式
初始化定式声明只有一个项: 一个 switch 表达式
这个 switch 表达式包含 枚举中每个元素的情况 以及返回 nil 的默认情况 我们需要为所有 这些情况创建语法节点
查找要创建的语法节点 有两个好方法 一是像我们之前那样打印语法树 二是阅读 SwiftSyntax 的文档
我们首先构建一个 InitializerDeclSyntax
这种类型可以通过 使用结果构建器来构建主体 并指定标题 也就是“init”关键字和所有参数 这将允许我们 在结果构建器中使用 for 循环 来迭代所有元素 正是我们所需要的
我只要从我们的测试案例中 复制 init 标题
在主体内部 我们需要一个 switch 表达式
这种类型还有一个带有标题 和结果构建器的初始化定式 让我们再次使用它
现在 我们可以通过迭代 之前收集的所有元素 来利用结果构建器的功能
对于每个元素 我们 希望创建一个新的案例项 我们可以使用字符串插值来构造它 就像之前的“#stringify”那样
我们还需要添加一个 返回 nil 的默认案例
最后 我们可以返回初始化定式
让我们运行测试 看看我们 是否真的生成了正确的初始化定式
没错 现在我们可以确定这个宏起作用了 我可以开始 在我的 App 中使用它了
为了将我们的宏包 添加到我的 Xcode 项目中 我可以右键它并选择 “Add Package Dependencies” 现在我可以选择 刚刚创建的本地包了
为了能够使用这个宏 我将 WWDC 目标 添加为我的 App 的依赖项
现在 我们可以从包中 导入 WWDC 模块 并将 SlopeSubset 宏 应用于 EasySlope 类型
……
如果我们开始编译…… 编译器会提醒我们手写的初始化定式 是一个无效的重复声明 而这是因为现在宏为我们生成了它 所以我们可以将其删除
删除代码总是很好玩 不是吗? 因此 如果我们想要 知道宏实际生成了什么 我们可以右键单击 SlopeSubset 并点按 Expand Macro
如果我忘记了这个宏的作用 我还可以按住 Option 键点按它 以阅读其文档
下一步应该是接着生成计算属性 但是我会晚些时候再做 通过使用宏 我们能够在 不编写重复代码的情况下 获得 EasySlopes 的类型安全性 我们是如何做到这一点的呢? 我们首先从 Swift 宏包模板开始 为了探索语法树的结构 我们中止了宏的执行 并在调试器中打印了语法节点 这使我们能够看到 我们需要访问哪些属性 来获取所有的枚举元素
而且使用测试用例 来独立开发宏真的很简单 在我们把它添加到 App 里之后 它直接就开始工作了 但是 如果在不支持的情况下 使用宏会发生什么呢? 就像你永远不想让初学者 滑到困难的坡道上一样 开发者也不想让 自己的宏执行意外的展开 或生成无法编译的代码 如果你的宏被用在它不支持的情况 一定要发出错误信息 告知你的采用者出了什么问题 而不是让他们阅读 生成的代码来调试你的宏
本着这种精神 让我们去修复 我们留在代码库中的 TODO 当 SlopeSubset 被应用于 非枚举类型时 宏应该发出一个错误 说明它只适用于枚举 就像之前一样 我们首先添加一个测试用例
这次 我们将 SlopeSubset 宏 应用于一个 struct
由于 struct 中没有枚举元素 我们不指望宏生成一个初始化定式 相反 它应该发出 一个诊断 也就是有一个错误 告诉我们 SlopeSubset 只能应用于枚举 如果我们运行这个测试…… 它会失败 因为我们还没有输出错误消息 让我们现在去编译器插件中 完成这个任务
宏错误可以由符合 Swift 错误协议的任何类型来表示 如果 SlopeSubset 应用于 非枚举类型 我将使用单个案例的枚举 来描述错误消息
如果我们从扩展函数中抛出错误 它将显示在调用宏展开的属性上
如果开发者想在属性 之外的不同位置显示错误消息、 生成警告 甚至在 Xcode 中显示 Fix-Its 可以使用上下文参数的 “addDiagnostic”方法 来生成丰富的诊断信息 但是我认为在这种情况下 仅在属性处显示一个简单的错误消息 才是有效的做法 现在我们来看看之前做的是否正确 以及我们的测试会不会通过
太好了 通过了 那么如果我将 SlopeSubset 应用于 一个 struct 它在 Xcode 中会怎样呢? 为此 让我将测试用例 复制到一个文件中
Xcode 会将自定义错误消息 与所有其他编译错误串联显示 这使我的宏的采用者可以 很容易地看到他们做错了什么
而且你知道吗? 现在我们有了优秀的错误处理能力 我认为这个宏对其他开发者 指定枚举子集也很有用 不仅仅是用在坡道上 让我们来泛化它吧
为了指定目前我们已经 硬编码为 Slope 的枚举的超集 我们在宏声明中添加一个泛型参数
由于这个宏不再专门用于坡道 让我们将它重命名为 EnumSubset 我们可以右键点按 SlopeSubset 然后选择 Refactor 和 Rename
我还可以重命名所有出现在 字符串字面量和注释中的内容 只要按住 Command 并点按它们即可
我们现在需要调整我们的宏实现 让其使用泛型参数 而不是硬编码的 Slope 类型 如果我们在调试器中打印属性 并检查其布局 就像我们对 “enumDecl”做的那样 我们可以通过访问属性名称中 “genericArgumentClause”的 第一个参数的 “argumentType” 来检索泛型参数 现在我们已经检索到了泛型参数 我们可以将此前 一直是硬编码的 Slope 类型 替换为 变量“supersetType”
我还需要进行几处修改 比如重新命名初始化定式的参数、 更改宏实现的类型名称 以及更新文档 我稍后会做这些工作 目前 我们要先确保 我们的测试仍然可以通过
由于我们将 EnumSubset 变成了泛型 我们需要将 slope 作为 泛型参数传递给 EnumSubset 宏 来显式指定 EasySlope 是 Slope 的一个子集
让我们看看现在的测试是否通过
通过了 我真的应该考虑把这个宏 作为 Swift 包发布给其他人 所以我们今天讨论了很多内容 让我们回顾一下我们都讲了什么 要创建一个宏 你可以从宏包模板开始 该模板包含的 stringify 宏 是一个很好的起点 在开发宏的时候 我们强烈建议开发者编写测试用例 以保证你的宏生成的代码确实有效 如果你这样做 你可以通过在扩展函数中设置断点、 运行测试 并在调试器中打印语法树 来检查语法树的布局 最后 如果开发者的宏 在某些情况下不适用 你应该始终发出自定义错误消息 这样即使出现问题 你的宏也会发挥作用 感谢观看 我很期待看到你创建的宏 ♪ ♪
-
-
5:55 - Invocation of the stringify macro
import WWDC let a = 17 let b = 25 let (result, code) = #stringify(a + b) print("The value \(result) was produced by the code \"\(code)\"")
-
6:31 - Declaration of the stringify macro
@freestanding(expression) public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "WWDCMacros", type: "StringifyMacro")
-
7:10 - Implementation of the stringify macro
public struct StringifyMacro: ExpressionMacro { public static func expansion( of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext ) -> ExprSyntax { guard let argument = node.argumentList.first?.expression else { fatalError("compiler bug: the macro does not have any arguments") } return "(\(argument), \(literal: argument.description))" } }
-
9:12 - Tests for the stringify Macro
final class WWDCTests: XCTestCase { func testMacro() { assertMacroExpansion( """ #stringify(a + b) """, expandedSource: """ (a + b, "a + b") """, macros: testMacros ) } } let testMacros: [String: Macro.Type] = [ "stringify": StringifyMacro.self ]
-
12:05 - Slope and EasySlope
/// Slopes in my favorite ski resort. enum Slope { case beginnersParadise case practiceRun case livingRoom case olympicRun case blackBeauty } /// Slopes suitable for beginners. Subset of `Slopes`. enum EasySlope { case beginnersParadise case practiceRun init?(_ slope: Slope) { switch slope { case .beginnersParadise: self = .beginnersParadise case .practiceRun: self = .practiceRun default: return nil } } var slope: Slope { switch self { case .beginnersParadise: return .beginnersParadise case .practiceRun: return .practiceRun } } }
-
14:16 - Declare SlopeSubset
/// Defines a subset of the `Slope` enum /// /// Generates two members: /// - An initializer that converts a `Slope` to this type if the slope is /// declared in this subset, otherwise returns `nil` /// - A computed property `slope` to convert this type to a `Slope` /// /// - Important: All enum cases declared in this macro must also exist in the /// `Slope` enum. @attached(member, names: named(init)) public macro SlopeSubset() = #externalMacro(module: "WWDCMacros", type: "SlopeSubsetMacro")
-
15:24 - Write empty implementation for SlopeSubset
/// Implementation of the `SlopeSubset` macro. public struct SlopeSubsetMacro: MemberMacro { public static func expansion( of attribute: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { return [] } }
-
16:23 - Register SlopeSubsetMacro in the compiler plugin
@main struct WWDCPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ SlopeSubsetMacro.self ] }
-
18:41 - Test SlopeSubset
let testMacros: [String: Macro.Type] = [ "SlopeSubset" : SlopeSubsetMacro.self, ] final class WWDCTests: XCTestCase { func testSlopeSubset() { assertMacroExpansion( """ @SlopeSubset enum EasySlope { case beginnersParadise case practiceRun } """, expandedSource: """ enum EasySlope { case beginnersParadise case practiceRun init?(_ slope: Slope) { switch slope { case .beginnersParadise: self = .beginnersParadise case .practiceRun: self = .practiceRun default: return nil } } } """, macros: testMacros ) } }
-
19:25 - Cast declaration to an enum declaration
guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { // TODO: Emit an error here return [] }
-
21:14 - Extract enum members
let members = enumDecl.memberBlock.members
-
21:32 - Load enum cases
let caseDecls = members.compactMap { $0.decl.as(EnumCaseDeclSyntax.self) }
-
21:58 - Retrieve enum elements
let elements = caseDecls.flatMap { $0.elements }
-
24:11 - Generate initializer
let initializer = try InitializerDeclSyntax("init?(_ slope: Slope)") { try SwitchExprSyntax("switch slope") { for element in elements { SwitchCaseSyntax( """ case .\(element.identifier): self = .\(element.identifier) """ ) } SwitchCaseSyntax("default: return nil") } }
-
24:19 - Return generated initializer
return [DeclSyntax(initializer)]
-
25:51 - Apply SlopeSubset to EasySlope
/// Slopes suitable for beginners. Subset of `Slopes`. @SlopeSubset enum EasySlope { case beginnersParadise case practiceRun var slope: Slope { switch self { case .beginnersParadise: return .beginnersParadise case .practiceRun: return .practiceRun } } }
-
28:00 - Test that we generate an error when applying SlopeSubset to a struct
func testSlopeSubsetOnStruct() throws { assertMacroExpansion( """ @SlopeSubset struct Skier { } """, expandedSource: """ struct Skier { } """, diagnostics: [ DiagnosticSpec(message: "@SlopeSubset can only be applied to an enum", line: 1, column: 1) ], macros: testMacros ) }
-
28:48 - Define error to emit when SlopeSubset is applied to a non-enum type
enum SlopeSubsetError: CustomStringConvertible, Error { case onlyApplicableToEnum var description: String { switch self { case .onlyApplicableToEnum: return "@SlopeSubset can only be applied to an enum" } } }
-
29:09 - Throw error if SlopeSubset is applied to a non-enum type
throw SlopeSubsetError.onlyApplicableToEnum
-
31:03 - Generalize SlopeSubset declaration to EnumSubset
@attached(member, names: named(init)) public macro EnumSubset<Superset>() = #externalMacro(module: "WWDCMacros", type: "SlopeSubsetMacro")
-
31:33 - Retrieve the generic parameter of EnumSubset
guard let supersetType = attribute .attributeName.as(SimpleTypeIdentifierSyntax.self)? .genericArgumentClause? .arguments.first? .argumentType else { // TODO: Handle error return [] }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。