大多数浏览器和
Developer App 均支持流媒体播放。
-
使用 Swift 设计协议接口
了解如何利用 Swift 5.7 设计采用协议的高级抽象概念。我们将向您介绍如何使用既存类型,探索如何从具有不透明结果类型的接口分离出实施,并分享有助于识别与保证具体类型之间关系的相同类型要求。为能更好地理解此讲座,我们建议您先观看 WWDC22 的“采用 Swift 泛型”。
资源
相关视频
WWDC23
WWDC22
-
下载
♪ ♪
Slava: 大家好 我是 Slava 来自 Swift 编译器团队 欢迎来到 使用 Swift 设计协议接口 我将接着“采用 Swift 泛型”的话题继续讨论 并向您展示一些 用于抽象具体类型的高级技术 并使用协议对类型关系建模 本次演讲将涵盖已有的语言功能 以及 Swift 5.7 中引入的一些新功能 本次演讲有三个主题 首先 我将通过解释 结果类型擦除的工作原理 向您展示带有联类型的协议 如何与存在的 any 类型交互 接下来 我将解释 使用不透明的结果类型 通过将接口 与实现分离来改进封装 对于最后一个话题 您将看到协议中的相同类型需求 如何对多个不同的具体类型集 之间的关系进行建模
让我们从学习具有关联类型的协议 如何与存在类型交互开始吧 这里 我们有一个数据模型 包含一对协议 和四种具体类型 这里有两种动物 鸡和牛 还有两种食物 鸡蛋和牛奶 鸡产蛋 牛产奶 为了对食物生产进行抽象 我将把 produce() 方法 添加到 Animal 协议中 您可能还记得在演讲 “采用 Swift 泛型” 中 抽象 Cow 和 Chicken 的 produce() 的不同返回类型的 最佳方法是使用关联类型 通过使用关联类型 我们声明 给定一些 Animal 的具体类型 调用 produce() 返回一些特定类型的 Food 这取决于具体的 Animal 类型 我们可以用图表来展示这种关系 协议 Self 类型代表 符合 Animal 协议的 实际的具体类型 Self 类型具有关联的 Commodity 类型 符合 Food 让我们看看具体的 Chicken 和 Cow 类型之间的关系 以及 Animal 协议的关联类型图
Chicken 类型符合 Animal 协议 其 CommodityType 为 Egg Cow 类型符合 Animal 协议 其 CommodityType 为 Milk 现在假设我们有一个充满动物的农场 农场上的 animals 存储属性 是 any Animal 的异构数组 在 ”采用 Swift 泛型” 中 我们看到了 any Animal 类型 如何拥有一个能够动态存储 任何具体动物类型的盒子表示 这种对不同具体类型 使用相同表示的策略叫做类型擦除
produceCommodities() 方法 映射动物数组 逐一调用 produce() 方法 该方法看起来很简单 但我们知道类型擦除 将消除与动物底层类型的 静态类型关系 因此值得深入挖掘 以了解此代码类型检查的原因
map() 闭包中的 animal 参数的 类型为 any Animal produce() 的返回类型是关联类型 当调用在存在类型上 返回关联类型的方法时 编译器将使用类型擦除 来确定调用的结果类型 类型擦除将这些关联类型 替换为具有等效约束的 相应存在类型 我们删除了具体的 Animal 类型 和关联的 CommodityType 之间的关系 将它们替换为 any Animal 和 any Food any Food 类型被称为 关联的 CommodityType 的上限 由于在 any Animal 上 调用了 produce() 方法 因此返回值是类型擦除的 从而为我们提供了 any Food 类型的值 这正是我们在这里期望的类型
让我们仔细看看关联类型擦除 是如何工作的 这是 Swift 5.7 中的一个新功能 出现在协议方法的 结果类型中的关联类型 在箭头的右侧 被称为处于“生成位置” 因为调用该方法将生成该类型的值 当在 any Animal 上调用此方法时 我们在编译时不知道具体的结果类型 但我们知道它是上限的子类型 在本例中 我们对运行时 持有 Cow 的 any Animal 调用 produce() 在我们的例子中 Cow 上的 produce() 方法返回 Milk Milk 可以存储在 any Food 中 这是 Animal 协议 关联的 CommodityType 的上限 对于所有符合 Animal 协议的 具体类型 这始终是安全的
另一方面 让我们考虑一下 如果关联类型出现在 方法或初始化程序的参数列表中 会发生什么 在这里 Animal 协议上的 eat() 方法 在食用位置具有关联的 FeedType 我们需要传入这种类型的值 来调用该方法 由于转换方向相反 因此无法执行类型擦除 关联类型的上限 存在类型 不会安全地转换为实际的具体类型 因为具体类型是未知的 我们来看一个例子 同样 我们有一个存储了 Cow 的 any Animal 假设 Cow 的 eat 方法需要 Hay Animal 协议关联的 FeedType 的 上限是 any AnimalFeed 但是给定一个任意的 any AnimalFeed 没有办法静态保证 它会存储 Hay 具体类型 类型擦除不允许我们在食用位置 使用关联的类型 相反 您必须通过 将存在的 any 类型传递给 采用不透明 some 类型的函数 来开箱 这种关联类型的类型擦除行为 实际上类似于 您可能在 Swift 5.6 中看到的现有语言功能 考虑一下用于克隆引用类型的协议 该协议定义了 一个单独的 clone() 方法 返回 Self 当您对 any Cloneable 类型的值 调用 clone() 时 结果类型 Self 将被类型擦除到其上限 Self 类型的上限始终是协议本身 因此我们收回一个 any Cloneable 类型的新值 总结一下 您可以使用 any 来声明一个值的类型 是存在类型 它存储了某个符合协议的具体类型 这甚至适用于具有关联类型的协议 当在生成位置调用 具有关联类型的协议方法时 关联类型被类型擦除到其上限 这是另一个承载 关联类型约束的存在类型 抽象具体类型不仅对函数输入有用 它对函数输出也很有用 因此具体类型仅在实现中可见 让我们看看如何抽象出 具体的结果类型 从而将一段代码的基本接口 与其实现细节分开 使静态类型分配在面对变化时 更加模块化 更健壮 让我们将 Animal 协议概括为 允许喂养动物 动物会饿 当它们饿的时候 它们需要吃东西 让我们在 Animal 协议中 添加一个 isHungry 属性 农场上的 feedAnimals() 方法 将喂养饥饿的动物子集 我已经将该饥饿动物子集的计算 拆分为一个 hungryAnimals 属性 hungryAnimals() 的这个初始实现 使用 filter() 方法 来选择 isHungry 属性为 true 的 动物子集 对 any Animal 数组调用 filter() 会返回一个新数组 any Animal 现在您可能会注意到 feedAnimals() 只迭代了一次 hungryAnimals 的结果 然后立即丢弃了这个临时数组 如果农场包含大量饥饿的动物 那么这将是低效的 避免这种临时分配的一种方法是 使用标准库的 lazy 集合功能 通过用 lazy.filter 替换对 filter 的调用 我们得到了所谓的 lazy 集合 lazy 集合与对 filter 的普通调用 返回的数组 具有相同的元素 但它避免了临时分配 但是现在必须将 hungryAnimals 属性的类型 声明为这个相当复杂的具体类型 LazyFilterSequence 这会暴露一个不必要的实现细节 客户端 feedAnimals() 并不关心 我们在 hungryAnimals 的 实现中使用了 lazy.filter 它只需要知道 它正在获取一些可以迭代的集合 不透明的结果类型可用于 将复杂的具体类型 隐藏在 Collection 的抽象接口后面 现在调用 hungryAnimals 的客户 只知道他们得到了一些 符合 Collection 协议的具体类型 但他们不知道具体的集合类型
然而 正如所写的那样 这实际上对客户端隐藏了 太多静态类型信息 我们声明了 hungryAnimals 输出的 一些符合 Collection 的 具体类型 但是我们对这个 Collection 的 Element 类型一无所知 在不知道元素类型 是 any Animal 的情况下 我们所能做的就是传递它 我们不能调用任何 Animal 协议的方法 让我们把重点放在 不透明的结果类型 some Collection 上面 通过使用约束不透明结果类型 我们可以在隐藏实现细节 和暴露足够丰富的接口之间 取得适当的平衡 约束不透明结果类型 是 Swift 5.7 中的新功能 通过在协议名称后的尖括号中 应用类型参数 来编写约束不透明结果类型 Collection 协议有一个类型参数 即 Element 类型 现在 一旦使用约束不透明结果类型 声明了 hungryAnimals 那么实际上 LazyFilterSequence 就会对客户端隐藏 但是客户端仍然知道它是某种 符合 Collection 的具体类型 其 Element 关联类型 等于 any Animal 这正是我们想要的接口 在 feedAnimals() 中的 for 循环中 animal 变量的类型为 any Animal 允许对每个饥饿的动物 调用 Animal 协议的方法 这一切都是有效的 因为 Collection 协议声明 Element 关联类型是主要关联类型 您可以通过在协议名称后的尖括号中 命名一个或多个关联类型来使用 主要关联类型声明您自己的协议 正如这里所示 最适合作为主要关联类型的关联类型 通常是那些由调用者提供的类型 例如集合的 Element 类型 而不是实现细节 例如集合的 Iterator 类型 通常 您会看到协议的主要关联类型 与符合该协议具体类型的 泛型参数之间的 对应关系 在这里 您可以看到 Collection 的 Element 主要关联类型 是由 Array 和 Set 的 Element 泛型参数实现的 这两个具体类型由标准库定义 都符合 Collection Collection 可以与使用 some 关键字的 不透明结果类型一起使用 也可以与使用 any 关键字的 约束存在类型一起使用 对于 Swift 5.7 之前的版本 您需要编写自己的数据类型 来表示具有特定泛型参数的存在类型 Swift 5.7 将此概念构建到 具有约束存在类型的语言中
如果我们希望 hungryAnimals 可以选择 是延迟还是即时地计算 hungryAnimals 使用不透明的 Collection 会导致函数 返回两种不同底层类型的错误 我们可以通过返回 any Collection 来解决这个问题 表明这个 API 可以在调用之间返回不同的类型 约束主要关联类型的能力 为不透明类型和存在类型 提供了新的表现力水平 这可以与各种标准库协议一起使用 例如 Collection 您还可以声明自己的协议 从而获得主要的关联类型 使用不透明类型编写泛型代码 必须依赖抽象类型关系 我们来讨论一下如何使用相关协议 来识别和保证多个抽象类型之间 必要的类型关系 我们将在 Animal 协议中 添加一个新的关联类型 用于该动物食用的 动物饲料的具体类型 以及一个告诉动物 食用这种类型饲料的 eat() 方法 为了让事情变得更有趣 我将引入一个额外的复杂功能 在我们能够喂养动物之前 我们必须种植合适类型的作物 并收获作物以生产饲料 这是第一个具体类型集 牛吃干草 所以如果有一头牛 我们首先需要种植一些干草 这给了我们苜蓿 苜蓿被收割并加工成 牛可以吃的干草 这是第二个具体类型集 鸡吃家禽饲料 所以如果您给我带来一只鸡 我们首先需要种植 一种名为小米的谷物 我们将其收获 并加工出家禽饲料 然后喂给我们的鸡 我想对这两个相关的 具体类型集进行抽象 这样我就可以实现一次 feedAnimal() 方法 让它同时喂牛和鸡 以及我将来可能收养的 任何新类型动物 由于 feedAnimal() 需要使用 Animal 协议的 eat() 方法 该方法在食用位置具有关联类型 因此我将通过 声明 feedAnimal() 方法 将 some Animal 作为参数类型 首先 我将定义一对协议 AnimalFeed 和 Crop 使用我们目前所知道的 协议和相关类型 AnimalFeed 有一个关联的 CropType 它符合 Crop 而 Crop 有一个关联的 FeedType 它符合 AnimalFeed 跟之前一样 我们可以查看 每个协议的类型参数图 首先 让我们看看 AnimalFeed 每个协议都有一个 Self 类型 它代表具体的符合类型 我们的协议有一个关联的 CropType 它符合 Crop 关联的 CropType 有一个符合 AnimalFeed 的 嵌套关联 FeedType 后者有一个符合 Crop 的 嵌套关联 CropType 以此类推 事实上 这种往复会一直持续下去 关联类型的无限嵌套在符合 AnimalFeed 和 Crop 之间交替
在 Crop 协议中也有类似的情况 只是移动了一个位置 我们从 Self 类型开始 符合 Crop 它有一个相关的 FeedType 符合 AnimalFeed 这有一个嵌套的关联 CropType 符合 Crop 以此类推
到无穷 让我们看看这些协议 是否正确地模拟了 具体类型之间的关系 回想一下 在我们喂养动物之前 我们需要种植作物 然后将其加工成正确类型的动物饲料 grow() 是 AnimalFeed 协议中的静态方法 这意味着它必须直接在 符合 AnimalFeed 的类型上调用 而不是在类型符合 AnimalFeed 的特定值上调用 我们需要写下一个符合 AnimalFeed 类型的名称 但我们只有一个特定的值 符合 Animal 的某种类型 一个不同的协议 那么 我们可以得到这个值的类型 我们知道它是 符合 Animal 的某种类型 而 Animal 有一个关联的 FeedType 它符合 AnimalFeed
此类型可用作 方法调用 grow() 的基础 AnimalFeed 上的 grow() 方法返回一个值 值的类型是 AnimalFeed 的 嵌套关联 CropType 我们知道 CropType 符合 Crop 所以我可以对它调用 harvest() 但我能收回什么呢 harvest() 被声明为 返回关联的 FeedType 符合 Crop 协议 在我们的例子中 由于调用的基础是 (some Animal).FeedType.CropType 因此 harvest() 将输出一个类型为 (some Animal).FeedType.CropType.FeedType 的值 不幸的是 这是错误的类型 (some Animal) 上的 eat() 方法 需要 (some Animal).FeedType 而不是 (some Animal).FeedType.CropType.FeedType 这段程序不是很类型友好 前面所写的这些协议定义 实际上并不能保证 如果我们从一种动物饲料开始 然后种植和收获这种作物 我们将得到与开始时相同的 动物饲料 这也是我们的动物期望吃到的 另一种思考方式是 这些协议定义过于笼统 它们没有准确地模拟 我们的具体类型之间的期望关系 为了理解原因 让我们看看 我们的 Hay 和 Alfalfa 类型 当我种干草时 我得到苜蓿 当我收获苜蓿时 我得到干草 以此类推 现在想象一下 我正在重构我的代码 并且不小心将 Alfalfa 上的 harvest() 方法的返回类型 更改为返回 Scratch 而不是 Hay 在这个意外更改之后 具体类型仍然满足 AnimalFeed 和 Crop 协议的要求 即使违反了我们期望的不变量 即种植和 收获作物会产生 与开始时相同类型的动物饲料 让我们再看一下 AnimalFeed 协议 这里真正的问题是 从某种意义上说 我们有太多不同的关联类型 我们需要写下这样一个事实 也就是其中两个关联类型 实际上是同一个具体类型 这将防止错误编写的具体类型 符合我们的协议 它还将为 feedAnimal() 方法 提供它需要的保证 我们可以使用写在 where 子句中的相同类型要求 来表达这些关联类型之间的关系 相同类型的要求表达了一种静态保证 即两个不同的 可能嵌套的关联类型 实际上必须是相同的具体类型 在此处添加相同类型的要求 会对符合 AnimalFeed 协议的 具体类型施加限制 在此相同类型的要求中 我们声明 Self.CropType.FeedType 与 Self 的类型相同 这在我们的图表中是什么样子的呢 我们可以做出这样的设想 每个符合 AnimalFeed 的具体类型 都有一个 符合 Crop 的 CropType 但是这个 CropType 的 FeedType 不仅仅是其他一些 符合 AnimalFeed 的类型 它与原始的 AnimalFeed 是相同的具体类型 我将所有关系折叠成 一对相关的关联类型 而不是嵌套关联类型的无限塔 那么 Crop 协议呢 在这里 Crop 的 FeedType 已折叠为一对类型 但我们仍然有一个过多的关联类型 我们想说 Crop 的 FeedType 的 Crop Type 与我们最初 开始使用的 Crop 类型相同
现在这两个协议已经具备了 相同类型的要求 我们可以再次访问 feedAnimal() 方法 跟之前一样 我们从某种动物的类型开始 我们得到了动物的饲料类型 我们知道它符合 AnimalFeed 协议 当种植这种作物时 我们会得到 一些动物饲料类型的作物类型 但是现在 当我们收获这种作物时 我们得到的正是动物 所期望的饲料类型 而不是另一个嵌套的关联类型 而快乐的动物现在可以保证 能够 eat() 我们刚刚种植的 正确类型的动物饲料 最后 让我们看一下 Animal 协议的关联类型图 它将我们迄今为止看到的 所有内容整合到一起
这里有两个符合类型的集 首先 我们有 Cow Hay 和 Alfalfa 其次 我们有 Chicken Scratch 和 Millet 请注意我们的三个协议 如何精确地模拟 每个集的三个具体类型之间的关系 通过了解您的数据模型 您可以使用相同类型的要求 来定义这些不同的 嵌套关联类型之间的等价关系 然后 当我们将对协议需求的 多个调用链接在一起时 泛型代码可以依赖于这些关系 在本期讲座中 我们探讨了何时类型擦除是安全的 以及何时我们需要处于 保证类型关系的上下文中 然后我们讨论了如何 在保留丰富类型信息 和使用主要关联类型 隐藏实现细节之间取得适当的平衡 主要关联类型可用于 不透明的结果类型和存在类型 最后 我们看到了如何在 表示这些相关类型集的协议中 使用相同类型的要求来识别 和保证具体类型集之间的类型关系 感谢您今天加入我的行列 希望您的 WWDC 之旅一切顺利
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。