大多数浏览器和
Developer App 均支持流媒体播放。
-
使用结果生成器在 Swift 中写入 DSL
通过创建自定义的编程语言或“域特定语言”,更轻松地解决一些问题。虽然创建 DSL 一般需要编写您自己的编译器,但您也可以使用运用 Swift 5.4 的结果生成器,以使您的代码更容易阅读和维护。我们将带您了解为 Swift 设计自定义语言的最佳实践:了解结果生成器和尾随闭包参数,探索修饰符式方法及其效果好的原因,并发现如何扩展 Swift 的正规语言规则,以将 Swift 变成 DSL。为了能充分了解本节内容,最好 (但非必需) 拥有一些 SwiftUI 视图编程经验。您不需要知道关于分析程序或编译器实现的任何内容。
资源
- Attributes - The Swift Programming Language
- Fruta: Building a Feature-Rich App with SwiftUI
- Result Builders - The Swift Programming Language
相关视频
WWDC21
WWDC19
-
下载
♪ (利用结果生成器 和Swift语言编写DSL) 大家好 我是贝卡 来自Swift编译器团队 今天我想讲解 如何使用Swift语言编写DSL 如果您对这个术语还不了解 DSL其实是一种特定域的编码语言 即使没听过它的名字 可能以前也曾使用过 我先解释一下DSL究竟是什么 在Swift语言环境中是什么样子 之后我会说明结果生成器的工作原理 是用于实现 Swift DSL的主要功能特点之一 之后 我会指导大家设计 一个简单的DSL 作为样本app Fruta的一部分 最后 我会为大家演示如何编写 Fruta示例代码中的实施方案 首先来介绍一下这个缩写 DSL是一种微型编程语言 是为一些特定区域而设计的 这些区域叫做"域" 因为这种语言 是为了完成特殊的工作而设计 所以有一些功能特点 能使工作内容更为简化 因此当使用DSL 而非通用语言编写代码时 你只需要编写 具体的问题 很多DSL是声明式的 也就是说 并不是编写精确的指令 来解决问题 更像是用语言描述问题 然后它来为你解决 这种传统的方式 叫做"单节点DSL“ 需要从头设计整个语言 并为其编写一个解释器或编译器 嵌入式DSL是更先进的选择 在嵌入式DSL中 使用像Swift这种 主语言的内置功能 将DSL的隐含行为 添加到代码的某些部分 能有效地将主语言调整为 为你所指定的域所需的定制语言 与设计整个语言相比 为其编写一个编译器 显然要容易得多 因为你是从一种现有语言开始的 已经确定了基本句法 并且已经有了一个编译器 这也使得DSL代码 与非DSL代码混合起来更为简单 通常想用DSL来解决的问题 只是庞大的app中的一部分 如果正在编写一个单节点DSL 必须设计一种方法 将一种语言调用为另一种语言 有了嵌入式DSL 用DSL编写的部分 对于app的其他部分来说 就像普通代码一样 交互起来也更容易 嵌入式DSL也可以使用 为主语言设计的工具 拥有了调试器 和Swift编辑器之后 在Swift DSL使用也很顺畅 如果想同时适用于单节点DSL 需要自己编写编译器 但因为是从主语言开始的 已经了解这种语言的用户 需要学习的新东西并不多 他们已经了解如何声明一个变量 或"else if"中是否有空格 他们需要学习的只有如何定制语言 设计Swift用来支持 嵌入式DSLs 其实 如果你用过SwiftUI 就已经接触过它了 SwiftUI的DSL视图 假设你想描述的是 设备屏幕上的视图布局 那么 当你在 SwiftUI DSL中编写时 自定义代码只是创建视图 而DSL则负责构建出树状结构 供SwiftUI处理 为了更好的了解DSL的价值 想象一下用普通Swift编写视图 SwiftUI会是什么样子 必须创建视图 修改 添加到其他视图 最后返回 还需要在各处创建临时变量 来保存单个视图 而且结果并不能像DSL那样 真正表达视图的嵌套方式 需要编写更多的代码 表达出来的信息反而更少 相比之下SwiftUI DSL 把所有这些乏味的细节都隐含其中 你只负责描述视图 DSL会负责收集你所描述的内容 并想办法显示出来 但DSL的运行需要多花费一些精力 使用起来也是如此 那什么情况应该创建DSL呢 并没有硬性的规定 但有一些迹象表明你可能需要DSL 比如那些用 vanilla Swift机制 掩盖了代码含义的地方 或者当你每次更改某些东西时 要花一半时间 重新安排逗号 方括号和小括号 或者不得不在临时数组中追加信息 来适应控制流的时候 还有这类情况 关于如何处理指令 直接编写不是最佳方式 而需要向代码的其他部分添加描述 比如在一个服务器端的网络框架中 可能有一个区域 为其支持的URL注册处理程序 不需要一次次调用添加处理程序 你可以为用户设计一个DSL 来声明每个URL与其处理程序 之后框架可以自动注册 无需编程人员 只需由初级工作人员 来查找代码中需要维护的部分 比如 你编写一个文本冒险游戏 大部分代码是由几个开发者编写的 而房间地图则由游戏设计师更新 NPC对话由编剧添加 也许有了DSL之后会使 他们的工作更简单易行 在使用DSL过程中 获益最多的情况 库便是个很好的例子 因为各种不同类型的用户都在使用 一个好的DSL 也可以处理项目中 自定义的大量内容 还有必须读取或经常更新 同时又想尽可能简化的内容 无论你创建DSL的目的是什么 不仅需要你花费精力去设计和实施 也需要用户花精力去学习 需要平衡好这一点 如果方法和数组常量 跟DSL几乎一样好用 那通常也是不错的选择 因为Swift程序员 已经很清楚它们的用法 但有时像在SwiftUI中 DSL是更好的选择 那么如何制作DSL呢 我们来拆解一下 Swift功能特性是如何用于 构建SwiftUI DSL的 Swift除了整体都很简洁的句法之外 SwiftUI DSL 还有以下四个优势 第一 属性包装器 这能让用户声明 与DSL行为相联系的变量 二 尾部闭包参数 它能让DSL提供函数或初始设定值 读取被添加到语言中的 自定义句法 三 结果生成器 收集DSL代码中计算出的值 变成一个返回值 方便处理 最后一点 修改器样式方法 基本上它们返回的是 所调用值的包装或修改版本 因为结果生成器 收集了代码计算的值 这个模式很有用 从2019年开始 在介绍讲座的后半部分 已经讨论过属性包装器 所以我今天不再赘述 但是关于其他三点 尤其是结果生成器 是今天的主题 对于尾部闭包和修改器方法 多数Swift程序员已经很熟悉了 但结果生成器是一个更幕后的功能 所以我们来讨论一下 如何利用它搭建DSL 结果生成器用来收集 DSL中创建的值 并将它们拼接成 你所使用的语言需要的任何数据结构 它们有点像 你可以声明 一个特殊类型 将其作为的属性包装器特性之一 具体来说 你可以将结果生成器 应用于几乎所有具有返回值的函数体 比如函数或方法 或者计算属性的获取器 或一个闭包 将结果生成器应用于函数体时 Swift会在结果生成器上 插入对静态方法的各种调用 最终捕捉到 本来会被剔除的声明结果 因此 通常会被Swift忽略的返回值 反而被传递给结果生成器 这些调用最终计算出一个值 并从函数体返回 当调用函数时 它会正常执行该函数中的所有语句 收集产生的值 并将它们合并成一个单一值 来作为闭包结果 结果生成器是一个编译时的功能 因此可以在app 运行的任何OS上工作 该功能的最终版本 来自开源的Swift Evolution 289号提案 包含在Swift 5.4中 在4月份搭载 Xcode 12.5发行 但该功能的原型在这之前就已经有了 所以你可能会看到一些 使用原型的旧教程或库 称之为”功能生成器“ 而不是”结果生产器“ 且有可能与最终功能并不完全匹配 所以我指出了之前 SwiftUI DSL中的功能 现在来说说它们是怎么工作的 我会简化一些细节 比如删除一些与SwiftUI类型 不相关的部分 显示一些假变量名称 如v0 用于编译器生成变量 这应该有助于了解基础知识 首先要认识到 在最高级别 虽然VStack有个区块 看上去像是新句法 但它其实是尾部闭包参数 如果我们查一下什么是VStack 就会知道它是SwiftUI中的结构 尾部闭包参数被传递给 该结构上的初始值设定项 现在我们来看一下 闭包被传递到的参数 我们注意到上面有一个 ViewBuilder属性 该属性告诉编译器 应该将名为ViewBuilder 的结果生成器 应用于闭包 ViewBuilder又是什么呢 根据这个名字查找类型 在SwiftUI中找到了 留意到它有一个 @resultBuilder属性 告诉编译器它是一个结果生成器 Swift既然已经定位了 结果生成器类型 就可以开始应用于闭包了 它做的第一件事是 为所有产生结果的语句创建变量 一旦创建完毕 编写调用ViewBuilder中 buildBlock方法 并将所有这些变量传递给它 buildBlock的工作 是将其所有参数处理 或合并成一个单一值 并将其返回 然后编译器会写一个返回语句 从闭包中返回 buildBlock的结果 基本上 编译器采用了你的代码 并添加了黄色代码 使得ViewBuilder 可以将你创建的所有值 集合成一个单一值 以此作为VStack的内容 现在 我要说明的是 修改器方法是如何适配进去的 修改器方法返回一个修改过的副本 或者返回一个被不同类型包裹的副本 来添加新的行为 并且使用的是 跟创建它时一样的声明 因此 它最终会在结果生成器接收前 改变数值 而且你可以在该方法的结果上 调用其他修改器方法 这样就能应用修改内容 将它们组合在一起 而且所有这些都是在 结果生成器接收值之前进行的 这两点 即组合修改器的能力 以及能在结果生成器 接收之前修改数值 正是Swift DSL经常 使用修改器的原因 这两点配合得很默契 现在 我们担心的一种情况是 假设我们设计的结果生成器是这样的 如果用户让你 彻底改变Swift的行为 他们不相信DSL 能像普通Swift代码一样工作 当我们设计结果生成器时 应该试图达到一种平衡 要有足够能力制作一个有效的DSL 还要确保Swift的功能 仍能按照用户的期望运行 结果生成器不会从根本上 重新诠释用户所写的代码 语句仍然以换行符结尾 调用仍然使用小括号 大括号仍然必须匹配 所有这些Swift句法的基础知识 都符合用户期望 也不会引入新的名称 这些名称在同一处 正常代码中是看不到的 有一些语言属性并没有意义 使用结果生成器时 大多数情况下 比如抓取或中断控制流时 其方式往往与捕捉 和使用语句结果不太相符 使用结果生成器时 这些功能会被禁用 还有一些功能特性 如if switch 和for-in语句 会被禁用 除非结果生成器 能提供了实现它们的其他方法 如果Swift 允许使用一个关键词 它就能按照正常方式工作 不会出现像if-else语句 那样的结果 运行真假区块 或循环跳过某些元素 结果生成器只是捕获了语句的结果 否则就会被去除 仅此而已 因此用户可以信赖它们
好了 现在我们知道了 什么是结果生成器 以及它的工作原理 可以开始设计 一个使用它们的DSL了 如果你从未使用过这种语言 可能会觉得这也太快了吧 但设计一个Swift DS 其实很像设计 一个Swift API 与Swift API一样 Swift DSL不是从零开始的 它使用Swift的句法和能力 来表述需要解决的 相关问题的想法和行为 DSL只是使用了 API通常不具备的附加能力 与Swift API一样 Swift DSL可以用 几种不同的方式进行设计 都能解决问题 你所要做的是想出替代方案 选择最佳方式 DSL只是提供了更多潜在解决方案 跟Swift API一样 Swift DSL的最佳经验法则 通常是选择 能产生最清晰的使用站点的设计 DSL假定用户会提前 花一些时间学习该语言 所以 对于从未使用过的人 是否已经了解清晰 它并不太重视 如果你以前设计过API 对于DSL来说 已经开了一个好头 就这一点而言 在DSL中使用过的建议和技术 可以很好地移植到API设计中 这次 我们将为Frutaapp 设计一个DSL 在Fruta的示例代码中 可以找到一个有效的实现 Fruta的源代码中 已经包含了15个奶昔食谱 在DSL之前 通过调用成员初始化器 并将其分配给一个静态常量 然后将所有的奶昔食谱 存储到另一个静态常量中 根据特定视图是否要 包括需在app内购买的食谱 以此来决定是返回整个列表 还是过滤掉付费食谱 这样就能完美运行了 想要的话还可以继续 但是奶昔食谱更新很频繁 不像其他食谱 由设计师 营销人员和经理更新 因此我们希望DSL 能使其不那么复杂 看看现在的做法 我不禁注意到一些缺点 从名单中过滤掉付费奶昔的需求 导致代码变形 allSmoothies和 hasFreeRecipe 只能在这个函数中使用 否则就不必存在 但只要想象一下没有它们的情况 就能明白不这么做的原因 创建数组 并向其中添加元素的机制 开始隐藏了 smoothies的真实列表 这也是这个函数的意义所在 同样的 将奶昔清单 与奶昔定义分开 不是明智的做法 其中有几个常数是用于预览的 但其中大多数只出现在这个列表中 而且因为你在一处 定义了一个smoothie 而在另一处把它添加到列表中 这很可能会出现错误 如果你声明了一个新的奶昔常量 但却忘了把它添加到列表中会怎样 或者同一个常量添加了两次呢 如果回头看看 对奶昔的单独定义 还有两处困扰我 一是配料表的字数多到无法想象 比如 每个条目都重复 "measure"这个词的某些版本出现了三次 在这一行 我们关心的信息是 1.5杯橙汁 这一行的其余部分 并没有什么有用的信息 只有视觉上的杂乱 不可避免地会有一些支持性句法 出现在重要信息周围 但如果有这么多周边信息 就会妨碍我们试图传达的信息 我注意到的另一个问题是 与实际想表达的信息量相比 每种奶昔所用的行数太多 我认为这里的罪魁祸首是 参数长度不一致 其中一些参数非常短 能合并在单行 其他的长一些 需要独占一行 现在 可以把短参数 合并到一行 然后用单独一行来写长参数 但大多数操作指南都不赞成这样做 我们希望有一种句法 能很自然的以不同方式来呈现 这些加起来 我们就有很多个目标 希望DSL能够实现 让维护奶昔清单变得更加简便 接下来要做的是 研究可以通过何种方式设计DSL 来实现这些目标 有大量不同的设计方式 能解决上述所有问题 我会快速解释 要实现前三个目标我会怎么做 再探讨实现最后一个目标的更多细节 我们要用所有的方法来定义奶昔清单 直接在主体定义 而不使用静态变量 这样就不用担心有人定义了一种冰沙 而忘记列出来 使用名为“奶昔数组生成器”的 结果生成器 来激活DSL 并收集成一个数组 这样就不需要使用数组字面 也不需要收集到一个临时变量中 允许沙冰加入in if声明 不需要像以前那样过滤列表 这样有个好处 已经了解Swift的用户 就知道怎样使用if语句 不了解的客户也很容易理解 "if includingPaid"是什么意思 我打算使用修改器方法 来指定原料的数量 原料会用到一个方法 ”measure(with:)“ 它接收一个单位 并返回一个包含该单位的计量的原料 如果需要不同数量的该单位 请使用scaled(by: ) 修饰符 在计量的配料上返回其数量 并乘以你传递的数字 这样 一杯橙汁变成1.5杯橙汁 一杯牛油果汁变成0.2杯牛油果汁 那么为什么scaled(by:) 是一个独立修饰符呢 Fruta屏幕上有一个控件 可用来缩放奶昔配方中的 原料数量 我们之前把乘数 传给了每一行的原料 使其数量成倍增加 但我意识到 实际上可以使用 scaled(by:)修饰符 代替scale the ingredients 这样就简化了行视图 通过对奶昔DSL设计进行一些调整 可以将其中一块 重新用于项目的另一部分 为实现前三个目标而做了调整之后 可以看到新的DSL成形了 现在我们集中精力完成最后一个目标 重新设计各个奶昔项目 使其更紧凑 并且尽量减少 混乱的符号 方便用户探寻 先看看可以用哪几种不同的方式 来安排这些信息 帮助实现这一目标 可以做的第一件事是 使用修饰符样式 添加描述和原料 很有用 但字数有点多 很容易让人忘记描述 或指定两次等等 还有个办法是给每个字段 设置一个标记类型 然后放入一个结果生成器的闭包中 但这会将ID和标题放在行上 最好不要这样做 或许可以把ID和标题 移回参数列表中 并对其他两个字段使用标记类型 但我觉得这与真正的需求相比 有点儿形式化 看到配方用户界面时我意识到 它们总是以特定顺序呈现 标题在顶部 描述在中间 原料表在下面 没必要在标题或描述上贴标签 让人们的视觉层次来决定 也就是让标题比描述 更显眼 我从中获得了灵感 决定 奶昔DSL也要这么做 把标题放在顶部 描述放在中间 原料表在底部 而且让描述在下面 比标题缩进得更多 这样在视觉上没有那么突出 传达了描述字符串的含义 所以不需要给它贴标签 结果显而易见 没有不必要的复杂情况出现 把它放到DSL整体背景中时 契合的赏心悦目 有人可能不同意 那也没关系 DSL是编程语言 个人喜好 主观权衡 是设计任何编程语言的重要部分 但这并不意味着放松要求 首先应该清楚地知道 想从语言中得到什么 研究是否可以用现有的解决方案 比如if语句 来解决问题 因为如果你采用的方案很常见 人们就不必去学习新方案 应该思考语言的每一部分 是如何与其他部分交互的 在Swift DSL中 也就是说要思考 你的DSL如何与周围的 普通Swift代码互动 比如我选scaled-by修饰符 是因为我可以在其他地方使用它 应该寻找 能在编写时发现错误 或者完全不会出错的解决方案 回想一下 这就是我们没有把描述 作为修饰符的原因 可能会不小心漏掉它 考虑到所有情况 应该能想出几种不同的可能性 设想每一种如何使用 编写小的模拟代码 反复对比衡量 但最后 通常不会出现一个明显正确 而其他明显错误的情况 你所能做的就是挑选出 你认为对使用这种语言的用户 最好的那个 如果你不确定哪一个最好 或许该选可读性最高的那个 如果还是不能确定 个人而言 我喜欢更大胆的选择 我宁可尝试一些东西 如果失败就重新来 也不愿永远不尝试 只留下疑问 现在我们已经决定了DSL的样子 继续把它添加到Fruta中 我已经将原有的奶昔定义替换成 使用我们的DSL的最终方法 但还没有真正实现DSL 不出意外 出现了一大堆错误 不过没关系 用它工作时 我会让这些错误带领我 找到需要解决的问题 最终得到一个没有任何错误的生成器 从函数顶部开始 第一个错误: “未知属性 ‘奶昔数组生成器‘” 结果生成器其实还不存在 当然会出现这个错误 我们来解决一下 首先建立一个类型 起名为”奶昔数组生成器“ 被标记为结果生成器属性 Swift不会真正做出 这种类型的实例 它只是存放静态方法的一个容器 所以我把它变成一个枚举 而且不定义任何情况 没有实例的情况下 不可能创建一个枚举 可以避免错误使用 如果只建立这个 会出现一个错误 结果生成器需要 buildBlock方法 这里有修复按钮 可以插入一个 我点击修复 然后再想办法实现它 还记得我们一开始说的吗 buildBlock的工作原理是 如果你有这样的代码 其中有一堆单独的语句 每个语句都会分配一个变量 这些变量都被 传递给buildBlock buildBlock返回的值 被闭包返回 因此 我们的buildBlock 需要接受一堆 奶昔作为参数 并返回一个奶昔数组 如果我们用一个变量参数 那么无论多少奶昔 都可以被传递到方法
建立 好了 现在好一点了 还是有很多错误 但提示”奶昔数组生成器“ 是无效属性的那条已经消失了 该属性的颜色都变了 表明它是一个已知类型 可以看下一条错误了 关于奶昔初始器的错误 写的是我们传递一个尾部闭包 到一个字符串参数 另一个说我们缺少 ”已测量原料“参数 显然 我们使用的是旧的初始器 它希望将描述和原料作为参数 我们需要建一个新的 因此 用一个ID 一个标题 和返回描述和原料的尾部闭包 来实现这个初始器 我现在就可以告诉你 我们以后还会回到这个初始器 如果我现在建立 确实清除了 所有来自奶昔初始器的错误 你可能觉得完美了 但这其实有一点误导大家 你看 还有一个错误 在这下面的if语句中 由于”奶昔数组生成器“ 没有完成导致的 而且因为有这个错误 Swift还没有检查闭包内部 就好像 如果我进入这个闭包 写一些已知不存在的随机变量名 然后构建它 Swift并没有标记错误 现在的情况是 Swift发现结果生成器 没有正确应用 所以它并不确信 在这些闭包中发现的错误 实际上是准确的 所以它并没有寻找错误 稍后 当我们完成奶昔数组生成器时 那些错误会突然出现 我们到时再修正它们 现在 继续做奶昔数组生成器会更容易一些 先把闭包的问题放一边 继续看下一个错误 这条错误 Swift告知我们 不能在奶昔数组生成器中 使用if语句 但可以通过添加一个方法来使用它 作为swift的特性之一 if语句是这样的 除非你的结果生成器 实施了其他方法来支持它们 否则它们是被禁用的 为了开始实施这一点 点击修复按钮看看它如何添加 很明显 我们需要实施一个方法 叫做buildOptional 它接收可选的奶昔数组 并返回一个奶昔数组 那这种方法是如何使用的呢 以这个简化的all方法为例 它只有一个if语句 没有其他 就像之前没有if语句的例子一样 这将把每个语句的结果 捕捉到一个变量中 把这些变量传递给 buildBlock 并从闭包中返回 buildBlock的结果 唯一的问题是 如何捕捉if语句的结果呢 要做的第一件事是 将if语句主体中的所有语句 抓取为变量 然后用buildBlock 将这些变量组合在一起 就像在最高级别里的做法一样 这就是buildOptional 的作用 Swift不再返回内部 buildBlock调用的结果 而是将其传递给 给 buildOptional buildOptional 返回的值 成为if语句的整体值 如果if条件是错误的 变量未被初始化 因此 buildOptional的参数 是可选的奶昔数组 Swift会添加一个else分支 将if语句的结果值 设置为buildOptional nil的返回值 对奶昔数组生成器来说 结果是 我们希望build Optional要么返回 buildBlock 传递给它的数组 要么在参数为零时返回一个空数组 如果现在构建 出现了 一个看起来非常奇怪的错误 不能将smoothie类型的 数组作为变量参数传递 什么情况 好吧 回到我们生成的代码 if语句最后出现了 一系列的奶昔字样 但实际上buildBlock 并不想要奶昔数组 它想要单个的奶昔 我们要修改一下 也许可以让buildBlock 将奶昔数组作为常量 然后使用 flatMap 将众多奶昔数组 串联起来 成为单一的奶昔数组 太好了 建立 不会吧 现在if语句开始起作用了 但所有带有奶昔的行都崩溃了 我们还需要这些 不能将奶昔类型的值 转换为奶昔数组 怎么回事呢 修改buildBlock 以便它与 buildOptional返回的 奶昔数组相匹配 但我们忘了 它还需要与 正常语句返回的 单个奶昔相匹配 糟糕 基本上 如果想让 任何复杂的控制流 buildBlock的返回类型 作为buildBlock的 参数来传递 有两种方式可以实现 一种是确保buildBlock 及其他结果生成器 返回的类型与 允许的语句兼容 例如 这是SwiftUI的 ViewBuilder的工作原理 在SwiftUI DSL中 所有内容都要符合视图协议 包括buildBlock 所返回的类型 及其他视图生成器方法 但这并不太适合我们的奶昔DSL 因为与SwiftUI视图不同 不能把奶昔字样 嵌在其他奶昔当中 还有一种方法是让结果生成器 将普通语句的值转换为 与buildBlock返回值的 类型相同 对这个DSL来说更适用 可以添加一个方法叫做 buildExpression 当添加该方法时 Swift将每个裸露的表达式 传递给该方法 然后将其捕捉到一个变量中 这样就有机会将其转换为数组 但是来自其他结果生成器方法的值 比如由buildOptional 和buildBlock产生的值 不会被这些提取中被调用 所以不会有这种转换 这是好事因为它们已经在返回数组了 我们要做的 是实现一个 buildExpression Xcode的代码完成功能 了解所有关于 结果生成器的方法 可以要求它写签名 然后将参数类型 改为奶昔 并直接返回 包含在数组字样当中的表达式参数 这样我们的单一smoothies 将被转化为buildBlock 需要的smoothies数组 创建……然后 好极了 if语句生效了 smoothie初始化程序 也生效了 可是如果在小地图上看 这里有第二个if语句 不可行 这是因为它有一个else子句 实际上buildOptional 只对普通的if语句有效 如果有一个else语句 或else-if 或一个开关 你需要实现两种方法 叫做 buildEither 1 和buildEither 2 试试用修复按钮创建 然后说一下它们如何工作 来看看 使用if-else语句 简化的例子 大部分转换就像 buildOptional一样 整个if-else语句 最终填充一个单一变量 跟buildOptional 还有一个相似点 if语句中的每一个区块 把其中的语句捕捉到变量中 然后利用buildBlock 合并成一个值 与普通if语句不同的是 没有用buildOptional 生成最终值 而是使用 buildEither方法 如果有两个分支 比如一个if和一个else 那么第一个使用 buildEither 1 第二个使用 buildEither 2 可以使结果生成器 根据所采用的分支来区分它们 现在 你可能想知道 如果有三个或更多条件 我们该怎么做 答案非常酷炫 我们要建立平衡二叉树 把每个分支作为一片叶子 然后将非叶子节点 作为要进行的调用 边缘会指出应该用 buildEither 1 还是buildEither 2 根据每个分支应该使用 哪一序列的调用 生成代码 用这个调用序列向变量赋值 尽管只有两个方法 结果生成器仍然可以区分 这三个分支 还不错 总之现在我们知道了 buildEither如何工作 我们可以继续编写 因为Smoothie ArrayBuilder 并不关心采取的是哪条分支 所以我们不需要做太多 只要返回数组参数就可以了
现在这个建好了 还是没有起到太大作用 但已经很接近了 大家应该还记得 我们遇到奶昔树组问题时 出现过这种错误 只不过这次 不是奶昔类型出错 而是类型'()'出错了 这是个空白元组 可能被认为是Void的类型 想想生成的代码 就会明白为什么会出现这样的问题 我们调用了 buildExpression 但被传递的表达式 叫做logger.log 它返回了Void 而不是Smoothie 所以我们要写一个build Expression重载 它会去掉一个Void参数 并返回一个空数组 然后重建 现在日志调用完美运行了 还有很多错误 但这是个好消息 这些错误中的第一类 是因为那个假变量 我刚开始添加它 是为了向大家展示Swift 没有在尾部闭合中 发现错误 现在 这意味着我们已经完成了 smoothie 数组生成器 来吧 改错吧 让我们删除假变量看看还有什么问题 如果仔细观察 就会发现问题比看上去要少 所有的描述行都出现了相同的警告 所有的原料行都有两个相同的错误 即使看起来有上百个错误 和一堆警告 其实只是几个相同的问题 重复出现 再仔细看看 其中一个原料行上的错误 编译器出现了两个问题 一 它不清楚要在 什么类型里找cups 二 它认为配料中没有 "measured." 这就说得通了 因为我们还没实现 measured(with: ) 或scaled(by: )修改器 所以它找不到 "measured." 它也不知道cups是什么 因为它不知道 measured(with:) 应该取体积单位 因此 跳到MeasuredIng redient.swift 运行这两个修改器 measured(with: ) 测量一种原料 返回MeasuredIngredient 并标上调用的单位 scaled(by: )继续执行 返回一个新的Measured Ingredient 并乘以调用比例的测量值 跳回Smoothie.swift 然后创建 好了 出现了更多的警告 但只有几个错误 仔细观察 会发现警告只有一种 说的是 闭包中的每个表达式 都被忽略了 错误也只有一种 闭包没有返回语句 为了查明原因 让我们来说说这些尾部闭包 以及结果生成器如何与它们交互 在这个例子中Smoothie ArrayBuilder 会像我们以前看到的那样 影响外部声明 它们被传递给 buildExpression 保存为变量 变量被传递给 buildBlock 那这些闭包呢 结果生成器会对它们做什么呢 好吧 什么也没做 因为闭包实际上是嵌套在 应用了结果生成器的函数中的 独立函数 结果生成器只适用于一个函数 不影响嵌套在其中的函数或闭包 如果你希望它们受结果生成器影响 就必须以其他方式应用 有三种方法可以将结果生成器 应用到函数体 第一种是将特性 直接写在函数或属性上 跟之前SmoothieArray Builder的改动一样 第二种方法 是写在协议要求的 函数或属性上 这样就会自动用来 实现所有符合要求的类型 这就是SwiftUIView中 主体属性的工作原理 ViewBuilder特性用于 View中的主体要求 所以也会自动应用于 所有View的主体属性 第三个方法 是把它写在一个封闭参数前面 如果这样做 Swift就会推断 任何传递给该参数的闭包 都应该有结果生成器来应用 如果Swift从协议或参数中 推断出一个结果生成器 而你其实并不希望它被应用 可以使用返回语句 明确地返回一个值来禁用它 但这个例子中 因为我们使用了闭包 我们要选择第三个方法 从闭包参数中推断出结果生成器 将特性写在 参数标签前 可以把Smoothie ArrayBuilder写在这 但这可能不是最佳方案 它会生成一系列smoothies 但我们不希望这个闭包 生成smoothies 我们想让它产生字符串和原料数组 而且也不希望这个闭包中 有if语句或 Void-returning调用 所以实际上 我们是在对这个闭包 应用一套单独的语言规则 而不是把第二套规则混入 SmoothieArray Builder 因此创建一个新的结果生成器 来实现这些新规则更行得通 把它叫做 SmoothieBuilder 为其创建一个新类型 并开始编写 buildBlock方法 这次有点特别 我们接受任意数量的Measured Ingredients 还想在前面抓取一个字符串 那怎么办呢 如果想想 SmoothieBuilder的功能 记住 它是一个简单的结果生成器 只有一个buildBlock方法 将被扩展出来 每一行都将作为 一个不同的参数传递 这样看来 或许可以在 buildBlock的开头 写一个字符串参数 第一条语句一定能产生一个字符串 而不是Measured Ingredient 我们试一下 在前面添加一个字符串参数 让它返回一个字符串 和一个原料数组的元组
这样 看吧 没有错误了 我们的DSL成功了 现在 结果生成器支持更多的功能 例如for-in循环 以及处理最终返回结果 如果想使用这些 可以参考《Swift编程语言》 在结束之前 我想请大家注意 编程语言设计中最重要的部分就是 有用的错误信息 设计一种语言时 你会学到一些 写无效代码的方法 比写有效代码的方法多得多 所以应该花一些时间 来思考无效代码发出的错误 代码错误时的做法 和正确时的做法同样重要 对于Swift DSL来说 它会免费教你如何处理错误 但用户得到的错误信息 是为一般的Swift代码设计的 不是以你的语言规则来表述的 所以它们可能无法 向用户清楚地表述问题 假如有人忘了 添加一个smoothies描述 Swift会发出一个错误信息 但这并不是很清晰 它说 第一个原料 不能被转换成字符串 那么 这段代码为什么会出现这个错误呢 Swift编译器并不能真正理解 我们的Smoothie DSL的语义 它只理解为使用结果生成器 而生成的Swift代码的语义 因此 试图诊断这个错误时 它认为这个值不是 对Smoothie的描述 也不是对第一种原料的描述 会认为它是buildBlock的 第一个参数 v0作为buildBlock的 第一个参数 是一个Measured Ingredient 但它被传递给一个字符串参数 所以Swift认为这个错误就像是 想把一个Measured Ingredient 传递给一个字符串参数 但我无法将Measured Ingredient转换为字符串 这个错误信息在技术上没有问题 但也没有什么用处 编译器工程师对此有一个窍门 我们让编译器支持无效的内容 但这样做 会产生一个错误 例如 Swift的函数语法中 有一个空格 可以写throws rethrows或nothing 如果编写其他不支持的单词 编译器会猜测 它应该是不同语句的一部分 并出现一个错误提示 让你加一个分号或者用一个新行 但如果你写上"try" 又会出现另一个错误 编译器建议将其替换为throws 然后解析文件其余部分 就像你已经写了throws一样 这是我们添加到 Swift解析器的一个特例 我们注意到开发者 想写throws时 有时会键入其他处理错误的关键字 所以我们对语言的形式语法 做了一次未经记录的小扩展 我们在这里解析这些错误的关键词 诊断出由失误产生的 不同于往常的错误 我指出这一点是因为 你也可以在结果生成器中这样做 来改善它的错误行为 具体来说 如果你做了一个 结果生成器方法的重载 与错误代码相匹配 然后把这个重载标记为不可用 你可以指定一个错误信息 以便在诊断时使用 这样用户得到的将不是 无法描述问题的通用错误 而是一个有针对性的 更具体的错误信息 我们要做的是 复制buildBlock 删除描述参数 这样就可以匹配只有原料表的区块 并用fatalError() 替换主体 从而不必伪造返回值 这个方法不可能被成功调用 所以主体必须是有效的 然后将这个重载标记为为不可用 并赋予它把问题描述得更清晰的信息 这个不可用的注释意味着 该方法实际上不能被使用 如果你编写了对它调用的代码 就会产生一个错误 现在如果我回到顶部并重建 会得到了一个更为清晰的 问题描述 写的不是 第一个原料应该是字符串 而是 描述字符串丢失 这样用户就不会认为 原料出错了 或者怀疑这个字符串的用意 因为错误信息很明确 这个体验会好很多 最需要记住的一点就是 对于实施DSL 一切都是为了改善用户体验 DSL可以使一些 非常复杂重复的代码 变得更加简洁 因为它允许用户设置自定义 不必担心 组装定义的原理 结果生成器是一个强大的工具 允许DSL收集被定义的值 而修改器方法 可以在结果生成器捕获值之前 用可组合的方式修改它们 请记住 如果你编写一个DSL 用户必须学习如何使用 所提供的DSL必须值得他们 花费时间和精力去学习 感谢您的观看 享受建立语言的乐趣吧 ♪
-
-
3:15 - FavoriteSmoothies view
struct FavoriteSmoothies: View { @EnvironmentObject private var model: FrutaModel var body: some View { SmoothieList(smoothies: model.favoriteSmoothies) .overlay( Group { if model.favoriteSmoothies.isEmpty { Text("Add some smoothies!") .foregroundColor(.secondary) .frame(maxWidth: .infinity, maxHeight: .infinity) } } ) .navigationTitle("Favorites") } }
-
3:38 - FavoriteSmoothies view (hypothetical alternative)
// Hypothetical code--not actually supported by SwiftUI struct FavoriteSmoothies: View { @EnvironmentObject private var model: FrutaModel var body: some View { var list = SmoothieList(smoothies: model.favoriteSmoothies) let overlay: View if model.favoriteSmoothies.isEmpty { var text = Text("Add some smoothies!") text.foregroundColor = .secondary var frame = Frame(subview: text) frame.maxWidth = .infinity frame.maxHeight = .infinity overlay = frame } else { overlay = EmptyView() } list.addOverlay(overlay) list.navigationTitle = "Favorites" return list } }
-
3:59 - FavoriteSmoothies view
struct FavoriteSmoothies: View { @EnvironmentObject private var model: FrutaModel var body: some View { SmoothieList(smoothies: model.favoriteSmoothies) .overlay( Group { if model.favoriteSmoothies.isEmpty { Text("Add some smoothies!") .foregroundColor(.secondary) .frame(maxWidth: .infinity, maxHeight: .infinity) } } ) .navigationTitle("Favorites") } }
-
6:17 - FavoriteSmoothies view
struct FavoriteSmoothies: View { @EnvironmentObject private var model: FrutaModel var body: some View { SmoothieList(smoothies: model.favoriteSmoothies) .overlay( Group { if model.favoriteSmoothies.isEmpty { Text("Add some smoothies!") .foregroundColor(.secondary) .frame(maxWidth: .infinity, maxHeight: .infinity) } } ) .navigationTitle("Favorites") } }
-
9:26 - Simple result builder example
VStack { Text("Title").font(.title) Text("Contents") }
-
9:36 - Simple result builder example + struct VStack
VStack { Text("Title").font(.title) Text("Contents") } struct VStack<Content: View>: View { … init(@ViewBuilder content: () -> Content) { self.content = content() } }
-
9:40 - Simple result builder example + struct VStack + trailing closure applied
VStack /* .init(content: */ { Text("Title").font(.title) Text("Contents") } /* ) */ struct VStack<Content: View>: View { … init(@ViewBuilder content: () -> Content) { self.content = content() } }
-
9:50 - Simple result builder example + struct VStack + trailing closure applied + enum ViewBuilder
VStack /* .init(content: */ { Text("Title").font(.title) Text("Contents") /* return // TODO: build results using ‘ViewBuilder’ */ } /* ) */ struct VStack<Content: View>: View { … init(@ViewBuilder content: () -> Content) { self.content = content() } } @resultBuilder enum ViewBuilder { static func buildBlock(_: View...) -> some View { … } }
-
VStack /* .init(content: */ { /* let v0 = */ Text("Title").font(.title) /* let v1 = */ Text("Contents") /* return ViewBuilder.buildBlock(v0, v1) */ } /* ) */ struct VStack<Content: View>: View { … init(@ViewBuilder content: () -> Content) { self.content = content() } } @resultBuilder enum ViewBuilder { static func buildBlock(_: View...) -> some View { … } }
-
14:49 - Fruta's smoothie lists, pre-DSL
// Fruta’s Smoothie lists extension Smoothie { static let berryBlue = Smoothie( id: "berry-blue", title: "Berry Blue", description: "Filling and refreshing, this smoothie will fill you with joy!", measuredIngredients: [ MeasuredIngredient(.orange, measurement: Measurement(value: 1.5, unit: .cups)), MeasuredIngredient(.blueberry, measurement: Measurement(value: 1, unit: .cups)), MeasuredIngredient(.avocado, measurement: Measurement(value: 0.2, unit: .cups)) ], hasFreeRecipe: true ) static let carrotChops = Smoothie(…) static let crazyColada = Smoothie(…) // Plus 12 more… } extension Smoothie { private static let allSmoothies: [Smoothie] = [ .berryBlue, .carrotChops, .crazyColada, // Plus 12 more… ] static func all(includingPaid: Bool = true) -> [Smoothie] { if includingPaid { return allSmoothies } logger.log("Free smoothies only") return allSmoothies.filter { $0.hasFreeRecipe } } }
-
14:50 - Fruta's smoothie lists, pre-DSL (hypothetical alternative)
// Fruta’s Smoothie lists (hypothetical alternative) extension Smoothie { static let berryBlue = Smoothie( id: "berry-blue", title: "Berry Blue", description: "Filling and refreshing, this smoothie will fill you with joy!", measuredIngredients: [ MeasuredIngredient(.orange, measurement: Measurement(value: 1.5, unit: .cups)), MeasuredIngredient(.blueberry, measurement: Measurement(value: 1, unit: .cups)), MeasuredIngredient(.avocado, measurement: Measurement(value: 0.2, unit: .cups)) ], hasFreeRecipe: true ) static let carrotChops = Smoothie(…) static let crazyColada = Smoothie(…) // Plus 12 more… } extension Smoothie { static func all(includingPaid: Bool = true) -> [Smoothie] { var allSmoothies: [Smoothie] = [ .berryBlue, .carrotChops, ] if includingPaid { allSmoothies += [ .crazyColada, // Plus more ] } else { logger.log("Free smoothies only") } return allSmoothies } }
-
14:51 - Fruta's smoothie lists, pre-DSL
// Fruta’s Smoothie lists extension Smoothie { static let berryBlue = Smoothie( id: "berry-blue", title: "Berry Blue", description: "Filling and refreshing, this smoothie will fill you with joy!", measuredIngredients: [ MeasuredIngredient(.orange, measurement: Measurement(value: 1.5, unit: .cups)), MeasuredIngredient(.blueberry, measurement: Measurement(value: 1, unit: .cups)), MeasuredIngredient(.avocado, measurement: Measurement(value: 0.2, unit: .cups)) ], hasFreeRecipe: true ) static let carrotChops = Smoothie(…) static let crazyColada = Smoothie(…) // Plus 12 more… } extension Smoothie { private static let allSmoothies: [Smoothie] = [ .berryBlue, .carrotChops, .crazyColada, // Plus 12 more… ] static func all(includingPaid: Bool = true) -> [Smoothie] { if includingPaid { return allSmoothies } logger.log("Free smoothies only") return allSmoothies.filter { $0.hasFreeRecipe } } }
-
18:05 - Near-final DSL design
// DSL top-level design @SmoothieArrayBuilder static func all(includingPaid: Bool = true) -> [Smoothie] { Smoothie( // TODO: Change these parameters id: "berry-blue", title: "Berry Blue", description: "Filling and refreshing, this smoothie will fill you with joy!", measuredIngredients: [ Ingredient.orange.measured(with: .cups).scaled(by: 1.5), Ingredient.blueberry.measured(with: .cups), Ingredient.avocado.measured(with: .cups).scaled(by: 0.2) ] ) Smoothie(…) if includingPaid { Smoothie(…) Smoothie(…) } else { logger.log("Free smoothies only") } }
-
19:57 - Possible DSL description/ingredient designs (start)
// Possible DSL description/ingredient designs Smoothie( id: "berry-blue", title: "Berry Blue", description: "Filling and refreshing, this smoothie will fill you with joy!", measuredIngredients: [ Ingredient.orange.measured(with: .cups).scaled(by: 1.5), Ingredient.blueberry.measured(with: .cups), Ingredient.avocado.measured(with: .cups).scaled(by: 0.2) ] )
-
20:11 - Possible DSL description/ingredient designs (modifiers)
// Possible DSL description/ingredient designs Smoothie(id: "berry-blue", title: "Berry Blue") .description("Filling and refreshing, this smoothie will fill you with joy!") .ingredient(Ingredient.orange.measured(with: .cups).scaled(by: 1.5)) .ingredient(Ingredient.blueberry.measured(with: .cups)) .ingredient(Ingredient.avocado.measured(with: .cups).scaled(by: 0.2))
-
20:25 - Possible DSL description/ingredient designs (all marker types)
// Possible DSL description/ingredient designs Smoothie { ID("berry-blue") Title("Berry Blue") Description("Filling and refreshing, this smoothie will fill you with joy!") Recipe( Ingredient.orange.measured(with: .cups).scaled(by: 1.5), Ingredient.blueberry.measured(with: .cups), Ingredient.avocado.measured(with: .cups).scaled(by: 0.2) ) }
-
20:36 - Possible DSL description/ingredient designs (some marker types)
// Possible DSL description/ingredient designs Smoothie(id: "berry-blue", title: "Berry Blue") { Description("Filling and refreshing, this smoothie will fill you with joy!") Recipe( Ingredient.orange.measured(with: .cups).scaled(by: 1.5), Ingredient.blueberry.measured(with: .cups), Ingredient.avocado.measured(with: .cups).scaled(by: 0.2) ) }
-
21:13 - Possible DSL description/ingredient designs (no marker types)
// Possible DSL description/ingredient designs 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) }
-
21:43 - Final DSL design
// DSL top-level design @SmoothieArrayBuilder static func all(includingPaid: Bool = true) -> [Smoothie] { 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) } Smoothie(…) { … } if includingPaid { Smoothie(…) { … } } else { logger.log("Free smoothies only") } }
-
24:05 - Basic SmoothieArrayBuilder
@resultBuilder enum SmoothieArrayBuilder { static func buildBlock(_ components: Smoothie...) -> [Smoothie] { return components } }
-
24:39 - How ‘buildBlock(…)’ works
// How ‘buildBlock(…)’ works @SmoothieArrayBuilder static func all(includingPaid: Bool = true) { /* let v0 = */ Smoothie(id: "berry-blue", title: "Berry Blue") { … } /* let v1 = */ Smoothie(id: "carrot-chops", title: "Carrot Chops") { … } // …more smoothies… /* return SmoothieArrayBuilder.buildBlock(v0, v1, …) */ }
-
25:03 - Basic SmoothieArrayBuilder
@resultBuilder enum SmoothieArrayBuilder { static func buildBlock(_ components: Smoothie...) -> [Smoothie] { return components } }
-
25:56 - Smoothie initializer (incomplete)
extension Smoothie { init(id: Smoothie.ID, title: String, /* FIXME */ _ makeIngredients: () -> (String, [MeasuredIngredient])) { let (description, ingredients) = makeIngredients() self.init(id: id, title: title, description: description, measuredIngredients: ingredients) } }
-
27:47 - SmoothieArrayBuilder with simple ‘if’ statements (incorrect)
@resultBuilder enum SmoothieArrayBuilder { static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] { return component ?? [] } static func buildBlock(_ components: Smoothie...) -> [Smoothie] { return components } }
-
28:01 - How ‘if’ statements work with ‘buildOptional(_:)’
// How ‘if’ statements work with ‘buildOptional(_:)’ @SmoothieArrayBuilder static func all(includingPaid: Bool = true) { /* let v0 = */ Smoothie(id: "berry-blue", …) { … } /* let v1 = */ Smoothie(id: "carrot-chops", …) { … } /* let v2: [Smoothie] */ if includingPaid { /* let v2_0 = */ Smoothie(id: "crazy-colada", …) { … } /* let v2_1 = */ Smoothie(id: "hulking-lemonade", …) { … } /* let v2_block = SmoothieArrayBuilder.buildBlock(v2_0, v2_1) v2 = SmoothieArrayBuilder.buildOptional(v2_block) */ } /* else { v2 = SmoothieArrayBuilder.buildOptional(nil) } */ /* return SmoothieArrayBuilder.buildBlock(v0, v1, v2) */ }
-
29:07 - SmoothieArrayBuilder with simple ‘if’ statements (incorrect)
@resultBuilder enum SmoothieArrayBuilder { static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] { return component ?? [] } static func buildBlock(_ components: Smoothie...) -> [Smoothie] { return components } }
-
29:28 - Why didn’t our ‘buildOptional(_:)’ work?
// Why didn’t our ‘buildOptional(_:)’ work? @SmoothieArrayBuilder static func all(includingPaid: Bool = true) { /* let v0 = */ Smoothie(id: "berry-blue", …) { … } /* let v1 = */ Smoothie(id: "carrot-chops", …) { … } /* let v2: [Smoothie] */ if includingPaid { /* let v2_0 = */ Smoothie(id: "crazy-colada", …) { … } /* let v2_1 = */ Smoothie(id: "hulking-lemonade", …) { … } /* let v2_block = SmoothieArrayBuilder.buildBlock(v2_0, v2_1) v2 = SmoothieArrayBuilder.buildOptional(v2_block) */ } /* else { v2 = SmoothieArrayBuilder.buildOptional(nil) } */ /* return SmoothieArrayBuilder.buildBlock(v0, v1, v2) */ }
-
29:40 - SmoothieArrayBuilder with simple ‘if’ statements (still incorrect)
@resultBuilder enum SmoothieArrayBuilder { static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] { return component ?? [] } static func buildBlock(_ components: [Smoothie]...) -> [Smoothie] { return components.flatMap { $0 } } }
-
30:14 - Why didn’t our ‘buildOptional(_:)’ work?
// Why didn’t our ‘buildOptional(_:)’ work? @SmoothieArrayBuilder static func all(includingPaid: Bool = true) { /* let v0 = */ Smoothie(id: "berry-blue", …) { … } /* let v1 = */ Smoothie(id: "carrot-chops", …) { … } /* let v2: [Smoothie] */ if includingPaid { /* let v2_0 = */ Smoothie(id: "crazy-colada", …) { … } /* let v2_1 = */ Smoothie(id: "hulking-lemonade", …) { … } /* let v2_block = SmoothieArrayBuilder.buildBlock(v2_0, v2_1) v2 = SmoothieArrayBuilder.buildOptional(v2_block) */ } /* else { v2 = SmoothieArrayBuilder.buildOptional(nil) } */ /* return SmoothieArrayBuilder.buildBlock(v0, v1, v2) */ }
-
31:23 - The ‘buildExpression(_:)’ method
// The ‘buildExpression(_:)’ method @SmoothieArrayBuilder static func all(includingPaid: Bool = true) { /* let v0 = SmoothieArrayBuilder.buildExpression( */ Smoothie(id: "berry-blue", …) { … } /* ) */ /* let v1 = SmoothieArrayBuilder.buildExpression( */ Smoothie(id: "carrot-chops", …) { … } /* ) */ /* let v2: [Smoothie] */ if includingPaid { /* let v2_0 = SmoothieArrayBuilder.buildExpression( */ Smoothie(id: "crazy-colada", …) { … } /* ) */ /* let v2_1 = SmoothieArrayBuilder.buildExpression( */ Smoothie(id: "hulking-lemonade", …) { … } /* ) */ /* let v2_block = SmoothieArrayBuilder.buildBlock(v2_0, v2_1) v2 = SmoothieArrayBuilder.buildOptional(v2_block) */ } /* else { v2 = SmoothieArrayBuilder.buildOptional(nil) } */ /* return SmoothieArrayBuilder.buildBlock(v0, v1, v2) */ }
-
31:44 - SmoothieArrayBuilder with simple ‘if’ statements (correct)
@resultBuilder enum SmoothieArrayBuilder { static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] { return component ?? [] } static func buildBlock(_ components: [Smoothie]...) -> [Smoothie] { return components.flatMap { $0 } } static func buildExpression(_ expression: Smoothie) -> [Smoothie] { return [expression] } }
-
32:48 - SmoothieArrayBuilder with ‘if’-‘else’ statements
@resultBuilder enum SmoothieArrayBuilder { static func buildEither(first component: [Smoothie]) -> [Smoothie] { return component } static func buildEither(second component: [Smoothie]) -> [Smoothie] { return component } static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] { return component ?? [] } static func buildBlock(_ components: [Smoothie]...) -> [Smoothie] { return components.flatMap { $0 } } static func buildExpression(_ expression: Smoothie) -> [Smoothie] { return [expression] } }
-
32:53 - How ‘if’-‘else’ statements work with ‘buildEither(…)’
// How ‘if’-‘else’ statements work with ‘buildEither(…)’ @SmoothieArrayBuilder static func all(includingPaid: Bool = true) -> [Smoothie] { /* let v0: [Smoothie] */ if includingPaid { /* let v0_0 = SmoothieArrayBuilder.buildExpression( */ Smoothie(…) { … } /* ) */ /* let v0_block = SmoothieArrayBuilder.buildBlock(v0_0) v0 = SmoothieArrayBuilder.buildEither(first: v0_block) */ } else { /* let v0_0 = SmoothieArrayBuilder.buildExpression( */ logger.log("Only got free smoothies!") /* ) */ /* let v0_block = SmoothieArrayBuilder.buildBlock(v0_0) v0 = SmoothieArrayBuilder.buildEither(second: v0_block) */ } /* return SmoothieArrayBuilder.buildBlock(v0) */ }
-
33:37 - How more complicated statements work with ‘buildEither(…)’
// How more complicated statements work with ‘buildEither(…)’ var v0: [Smoothie] switch userRegion { case .americas: // ...smoothies omitted... /* let v0_block = SmoothieArrayBuilder.buildBlock(...parameters omitted...) v0 = SmoothieArrayBuilder.buildEither(first: SmoothieArrayBuilder.buildEither(first: v0_block)) */ case .asiaPacific: // ...smoothies omitted... /* let v0_block = SmoothieArrayBuilder.buildBlock(…) v0 = SmoothieArrayBuilder.buildEither(first: SmoothieArrayBuilder.buildEither(second: v0_block)) */ case .eastAtlantic: // ...smoothies omitted... /* let v0_block = SmoothieArrayBuilder.buildBlock(…) v0 = SmoothieArrayBuilder.buildEither(second: v0_block) */ }
-
34:12 - SmoothieArrayBuilder with ‘if’-‘else’ statements
@resultBuilder enum SmoothieArrayBuilder { static func buildEither(first component: [Smoothie]) -> [Smoothie] { return component } static func buildEither(second component: [Smoothie]) -> [Smoothie] { return component } static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] { return component ?? [] } static func buildBlock(_ components: [Smoothie]...) -> [Smoothie] { return components.flatMap { $0 } } static func buildExpression(_ expression: Smoothie) -> [Smoothie] { return [expression] } }
-
34:54 - How ‘if’-‘else’ statements work with ‘buildEither(…)’
// How ‘if’-‘else’ statements work with ‘buildEither(…)’ @SmoothieArrayBuilder static func all(includingPaid: Bool = true) -> [Smoothie] { /* let v0: [Smoothie] */ if includingPaid { /* let v0_0 = SmoothieArrayBuilder.buildExpression( */ Smoothie(…) { … } /* ) */ /* let v0_block = SmoothieArrayBuilder.buildBlock(v0_0) v0 = SmoothieArrayBuilder.buildEither(first: v0_block) */ } else { /* let v0_0 = SmoothieArrayBuilder.buildExpression( */ logger.log("Only got free smoothies!") /* ) */ /* let v0_block = SmoothieArrayBuilder.buildBlock(v0_0) v0 = SmoothieArrayBuilder.buildEither(second: v0_block) */ } /* return SmoothieArrayBuilder.buildBlock(v0) */ }
-
35:07 - SmoothieArrayBuilder with support for ‘Void’ results
@resultBuilder enum SmoothieArrayBuilder { static func buildEither(first component: [Smoothie]) -> [Smoothie] { return component } static func buildEither(second component: [Smoothie]) -> [Smoothie] { return component } static func buildOptional(_ component: [Smoothie]?) -> [Smoothie] { return component ?? [] } static func buildBlock(_ components: [Smoothie]...) -> [Smoothie] { return components.flatMap { $0 } } static func buildExpression(_ expression: Smoothie) -> [Smoothie] { return [expression] } static func buildExpression(_ expression: Void) -> [Smoothie] { return [] } }
-
36:41 - Modifier-style methods on Ingredient and MeasuredIngredient
extension Ingredient { func measured(with unit: UnitVolume) -> MeasuredIngredient { MeasuredIngredient(self, measurement: Measurement(value: 1, unit: unit)) } } extension MeasuredIngredient { func scaled(by scale: Double) -> MeasuredIngredient { return MeasuredIngredient(ingredient, measurement: measurement * scale) } }
-
37:32 - Closures and result builders
// Closures and result builders @SmoothieArrayBuilder static func all(includingPaid: Bool = true) -> [Smoothie] { /* let v0 = SmoothieArrayBuilder.buildExpression( */ Smoothie(…) { "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) } /* ) */ /* let v1 = SmoothieArrayBuilder.buildExpression( */ Smoothie(…) { "Packed with vitamin A and C, Carrot Chops is a great way to start your day!" Ingredient.orange.measured(with: .cups).scaled(by: 1.5) Ingredient.carrot.measured(with: .cups).scaled(by: 0.5) Ingredient.mango.measured(with: .cups).scaled(by: 0.5) } /* ) */ /* return SmoothieArrayBuilder.buildBlock(v0, v1) */ }
-
39:22 - Smoothie initializer (final) and SmoothieBuilder (initial)
extension Smoothie { init(id: Smoothie.ID, title: String, @SmoothieBuilder _ makeIngredients: () -> (String, [MeasuredIngredient])) { let (description, ingredients) = makeIngredients() self.init(id: id, title: title, description: description, measuredIngredients: ingredients) } } @resultBuilder enum SmoothieBuilder { static func buildBlock(_ description: String, components: MeasuredIngredient...) -> (String, [MeasuredIngredient]) { return (description, components) } }
-
40:38 - Accepting different types
// Accepting different types Smoothie(…) /* @SmoothieBuilder */ { /* let v0 = */ "Filling and refreshing, this smoothie will fill you with joy!" /* let v1 = */ Ingredient.orange.measured(with: .cups).scaled(by: 1.5) /* let v2 = */ Ingredient.blueberry.measured(with: .cups) /* let v3 = */ Ingredient.avocado.measured(with: .cups).scaled(by: 0.2) /* return SmoothieBuilder.buildBlock(v0, v1, v2, v3) */ }
-
41:01 - Smoothie initializer (final) and SmoothieBuilder (initial)
extension Smoothie { init(id: Smoothie.ID, title: String, @SmoothieBuilder _ makeIngredients: () -> (String, [MeasuredIngredient])) { let (description, ingredients) = makeIngredients() self.init(id: id, title: title, description: description, measuredIngredients: ingredients) } } @resultBuilder enum SmoothieBuilder { static func buildBlock(_ description: String, components: MeasuredIngredient...) -> (String, [MeasuredIngredient]) { return (description, components) } }
-
42:43 - SmoothieBuilder without the string
// SmoothieBuilder without the string Smoothie(…) /* @SmoothieBuilder */ { // "Filling and refreshing, this smoothie will fill you with joy!" /* let v0 = */ Ingredient.orange.measured(with: .cups).scaled(by: 1.5) /* let v1 = */ Ingredient.blueberry.measured(with: .cups) /* let v2 = */ Ingredient.avocado.measured(with: .cups).scaled(by: 0.2) /* return SmoothieBuilder.buildBlock(v0, v1, v2) */ } extension SmoothieBuilder { static func buildBlock(_ description: String, _ ingredients: ManagedIngredients...) -> (String, [ManagedIngredients]) { … } }
-
43:38 - How Swift improves diagnostics
// How Swift improves diagnostics func fn0() throws {} func fn1() rethrows {} func fn2() {} func fn3() deinit {} func fn4() try {}
-
44:30 - SmoothieBuilder without the string
// SmoothieBuilder without the string Smoothie(…) /* @SmoothieBuilder */ { // "Filling and refreshing, this smoothie will fill you with joy!" /* let v0 = */ Ingredient.orange.measured(with: .cups).scaled(by: 1.5) /* let v1 = */ Ingredient.blueberry.measured(with: .cups) /* let v2 = */ Ingredient.avocado.measured(with: .cups).scaled(by: 0.2) /* return SmoothieBuilder.buildBlock(v0, v1, v2) */ } extension SmoothieBuilder { static func buildBlock(_ description: String, _ ingredients: ManagedIngredients...) -> (String, [ManagedIngredients]) { … } @available(*, unavailable, message: "missing ‘description’ field") static func buildBlock(_ ingredients: ManagedIngredients...) -> (String, [ManagedIngredients]) { fatalError() } }
-
44:55 - Smoothie initializer (final) and SmoothieBuilder (with error handling)
extension Smoothie { init(id: Smoothie.ID, title: String, @SmoothieBuilder _ makeIngredients: () -> (String, [MeasuredIngredient])) { let (description, ingredients) = makeIngredients() self.init(id: id, title: title, description: description, measuredIngredients: ingredients) } } @resultBuilder enum SmoothieBuilder { static func buildBlock(_ description: String, components: MeasuredIngredient...) -> (String, [MeasuredIngredient]) { return (description, components) } @available(*, unavailable, message: "first statement of SmoothieBuilder must be its description String") static func buildBlock(_ components: MeasuredIngredient...) -> (String, [MeasuredIngredient]) { fatalError() } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。