大多数浏览器和
Developer App 均支持流媒体播放。
-
采用 Swift 泛型
泛型是在 Swift 中编写抽象代码的基础工具。了解如何随着代码的发展识别抽象的机会,评估编写一段具有多项行为的代码的策略,并且探索 Swift 5.7 中可帮助您更轻松编写与理解泛型代码的语言功能。
资源
相关视频
WWDC23
WWDC22
-
下载
♪ 柔和乐器演奏的嘻哈音乐 ♪ ♪ 大家好 我是 Swift 编译器团队的 Holly 欢迎来到采用 Swift 泛型的课程 泛型是一个基本工具 用它在 Swift 中编写抽象代码 随着您编程的发展 泛型在 管理复杂度方面必不可少 抽象将概念与具体细节分开 在编程中 抽象在很多方面 都很有用 您常用的一种抽象形式可能是 将代码重构到函数或局部变量中 如果您要反复用到 同个功能或值 这种抽象会非常有用 当您将功能提取到函数中时 细节会抽离出来 然后用着抽象的代码 可以表达进行事项的概念 且不用重复细节 在 Swift 中 您还可以 抽象出具体类型 如果您有一组相同概念的类型 有着不同的细节 您可以写抽象代码 使用所有这些具体类型 我们今天将介绍 通过具体类型建立代码模型的工作流程 识别一组具体类型的通用能力 构建表示上述能力的接口 最后 我们将深入探讨使用 该接口编写泛型代码 我们将深入研究 Swift 的抽象工具 同时写些代码来模拟一个农场 那么 我们先从编写具体类型开始 我们从名为 “Cow” 的结构入手 Cow 有一种方法 “eat” 它接受 Hay 类型的参数 Hay 是另一个结构体 它有一个 “grow” 的静态方法 种植生产 Hay 的作物 这个作物是 Alfalfa Alfalfa 结构体有 从 Alfalfa 实例 收获 Hay 的方法 最后 我们再添加个 “Farm” 的结构体 有喂养牛的方法 feed 方法可以通过以下步骤实现 先是种植苜蓿来生产干草 然后是收割干草 最后 把干草喂给牛 这样我就可以在农场喂养牛了 但我想添加其他种类的动物 我可以添加新的结构体 来代表其他动物 比如 Horse 和 Chicken 我想要在农场喂养牛 马和鸡 我可以超载 feed 方法 来分别接受每种类型的参数 但每个超载都有非常相似的实现 随着我添加更多类型的动物 这会成为额外的负担 而且多数是重复的代码 如果您发觉自己在编写的重载 有着重复的实现 这很可能是要泛化的迹象 本质上 这些实现十分相似 因为不同类型的动物 功能是相似的 下一步是识别动物类型 之间的通用功能 我们建立了一组动物类型 不同动物都会吃某类食物 不同动物有不同的进食方法 所以 eat 方法的不同实现 会有行为上的差异 我们想要的 是允许抽象代码调用 eat 方法 并使得抽象代码根据其 运行着的具体类型体现不同的行为 抽象代码能够面向不同的具体类型 拥有不同的行为 就叫作“多态” 多态允许一段代码根据其使用方式 有不同行为 多态本身有不同的形式 第一个是函数超载 相同的函数调用根据参数类型 能代表不同的事物 超载称为“特定多态” 因为超载不是能通用的解决方案 我们刚才也看到了超载 可能会导致代码重复 下一个是子类多态 这是在超类型上运行的代码 运行该代码时 子类多态根据 特定的子类型 可以有不同的行为 最后一个是参数多态 是运用泛型实现的 泛型代码使用类型参数来编写 一段适用于不同类型的代码 并且具体类型本身也作为参数 我们已经排除了超载的可能性 所以我们来尝试使用子类多态 表示子类型关系的方法之一 是带有类层次结构 我们可以引入一个 “Animal” 的类 然后 我们把每一动物 类型的结构体改为类 每个具体的动物类都将继承 来自 Animal 的超类 并重载 eat 方法 现在 我们有一个 抽象基类 Animal Animal 可以代表 任一特定的动物类型 在 Animal 类上 调用 eat 的代码 将用到子类多态 以调用子类实现 但这还不是结束 我们还没有填写在 Animal 里 eat 方法的参数类型 此代码中还有其他问题值得担忧 第一个问题 运用类 迫使我们采用引用语义 即便我们不需要或不想要不同 动物实例之间共享任何状态 第二 子类多态还需要子类 重载基类中的方法 但忘记这么做的话 直到运行那时才会被抓住 第三 这种抽象模型还有更大的问题 就是每种动物子类型 吃的食物类型不同 而且这种依赖关系真的很难 用类层次结构表达 我们可以采取的办法是让方法接受 不太具体的类型 例如 Any 但这个策略依赖于子类的实现 以确保运行时是正确的类型 那么 我们在每个重载的方法里 塞进了额外的重复代码 但重点是这么做 会让您无意中传递错误类型的食物 给您留下另一个 只能在运行时才发现的漏洞 因此 我们试试别的方式吧 我们可以用类型安全的方式 来表达动物的饲料类型 只要在动物 (Animal) 超类上引入类型参数 类型参数相当于占位符 作为每个子类的具体的饲料类型 这个方法下 食物 (Food) 类型参数 必须提升至动物 (Animal) 类的声明位置 这看起来有点奇怪 因为虽然动物需要食物来运作 但进食不是动物的核心目标 而且很多用于动物的代码 可能跟食物根本没关系 尽管如此 所有对 动物 (Animal) 类的引用 都需要明确食物 (Food) 类型 例如 每个动物 (Animal) 子类 需要在继承子句中用尖括号 明确指出其食物类型 在每个用到动物 (Animal) 类的 地方都要重复这种操作 可能会变得很琐碎 尤其是我们在给 每种动物添加更多特定类型的时候 综上 上面用到的方法 都没有体现良好的工效学 或正确的语义 其根本问题是类属于数据类型 同时我们一直试图在超类上纠缠 以让它代表关于具体类型的抽象概念 取而代之 我们 想要的是一个语言构造 来表示类型的能力 而无需知道这个能力具体如何运作 动物有两个共同的能力 一是每种动物都有特定类型的食物 二是各种动物都会有食用食物的操作 我们可以构建一个 代表这些能力的接口 在 Swift 中可以通过协议完成 协议是一种抽象工具 它描述了遵守协议的类型的功能 使用协议 您可以把 类型功能的概念与实现细节中分离 关于类型功能的概念 是通过接口表示的 我们将动物的能力转译 成一个协议接口 协议名称 代表着我们描述类型的类别 因此我称这个协议为 “Animal” 每个能力会映射为一个协议需求 具体的食物种类会映射 到 Animal 协议的一个关联类型 就像类型参数一样 关联类型 相当于具体类型的占位符 让关联类型变得特别的 是它们依赖特定的类型 这个类型遵守协议 这关系是有保证的 所以具体动物类型的每个实例 都拥有相同类型的食物 接着 进食的操作会映射到一个方法 这个方法是 “eat” 它接受动物 feed 类型的参数 协议里没有该方法的实现 需要具体的动物类型来实现它 现在有了 Animal 协议 我们可以让每个具体 的动物类型都遵守协议 您可以在声明或扩展中使用 协议符合性注解具体类型 协议不限于类 结构体、枚举和 actor 也都可以使用协议 编写符合性注释后 编译器会检查确认具体类型 实现了每个协议的要求 每种动物类型都必须 实现 eat 方法 而且编译器可以推断 feed 类型是什么 因为参数列表中使用了 feed 类型 feed 类型也可以 用类型别名显式写明 我们成功识别了 动物的共同功能并用协议接口 表达了出来 现在我们可以开始编写泛型代码了 我们可以用 Animal 协议 在 Farm 里实施 feed 方法 我们想写的实现 是适用于所有具体动物类型的 我们会用到参数多态 然后引入一个类型参数 在调用方法时 会被具体类型替代 类型参数放在函数名称之后 用尖括号括起来 就跟正常变量和函数参数一样 您可以随意命名类型参数 还有跟其他类型一样 您可以在函数签名中 随意用类型参数名称引用这个参数 示例中 我声明了一个 类型参数 叫作 “A” 我把 A 当作动物函数参数的类型 我们要一直保持具体的动物类型 符合 Animal 协议 所以我们 用协议符合性标注类型参数 协议符合性可以写在尖括号里 或者写在行尾的 “where” 子句中 写在子句可以 指定不同类型参数之间的关系 命名类型参数跟行尾 “where” 子句 是非常有用的 因为这组合能让您编写 复杂的要求和类型关系 但是其实多数的泛型函数 不需要这种程度的泛型 我们来仔细研究 feed 方法 类型参数 A 在 参数列表中出现一次 然后 “where” 子句在类型参数上 列出了符合性要求 这种情况下 命名类型参数加上使用的 “where” 子句 让这个方法看上去比实际上更复杂 这种泛型模式很常见 所以其实有一种更简单的表达方式 不用显式编写类型参数 我们可以在通过协议符合性 表达抽象类型的时候 直接写成 some Animal 这个声明跟前面的是一样的 但类型参数列表 和 “where” 子句都不再需要了 因为我们不需要用到 它俩提供的表现能力 写成 “some Animal” 更为直接 因为这样减少了语法噪音 而且 “some Animal” 就在参数声明中 直接给出了关于 动物参数的语义信息 我们来分开看看 “some Animal” 语法 “some Animal” 中的 “some” 代表有您正在用的 特定类型 “some” 关键字始终遵循 符合性要求 这个示例里 特定类型必须符合 动物协议 这允许我们 运用动物协议中 关于参数值的要求 “some” 关键字可 用在参数和结果类型 如果您之前写过 SwiftUI 代码 您已在结果位置用过 “some” 了 用的是 “some View” “some View” 的结果类型 是完全相同的概念 在 SwiftUI 视图中 body 属性 返回某种特定类型的视图 但是用了 body 属性的代码 不需要知道具体的类型是什么 我们先退一步来看 以更好地理解特定抽象类型的概念 表示特定具体类型 占位符的抽象类型 称为不透明类型 代换的特定具体类型 称为基础类型 对于不透明类型的数值 其基础类型在数值的作用域中是确定的 这样 可以保证用该值的泛型代码 在每次访问这个值时 都获得相同的基础类型 有 “some” 关键字的类型和 在尖括号里的命名类型参数 都声明了不透明类型 不透明类型可用于输入和输出 所以此类型可以在参数位置 或结果位置声明 函数箭头是区分 两个不同位置的标志 不透明类型的位置决定 程序的哪个部分看到了抽象类型 以及程序的哪个部分决定了具体类型 命名类型参数只在输入端声明 所以调用者决定基础类型 实现用的是抽象类型 一般情况下 提供数值的程序 决定不透明参数 而结果类型 决定基础类型的情况下 使用该值的部分 可以看到抽象类型 我们来深入了解该工作原理 跟随我们对参数和结果值的直觉 因为基础类型是从一个值推导出来的 基础类型跟值总是 来自同一个地方 对局部变量来说 基础类型 从赋值右侧的值中推导 这意味着有不透明类型的局部变量 必须有一个初始值 如果您不提供一个初始值 编译器就会报错 基础类型在变量的作用域内 必须是固定的 所以试图改变基础类型 也会导致错误 对于不透明类型的参数 基础类型 从调用点的参数值中推导 在参数位置使用 “some” 是 Swift 5.7 里的新功能 基础类型只要 在参数范围内保持不变就行 因此每次调用 都可以提供不同的参数类型 对于不透明的结果类型 基础类型 从实现的返回值中推导出 具有不透明结果类型 的方法或计算属性 可以在程序中的任何位置调用 因此这个命名值的作用域是全局的 这意味着基础返回类型在所有 返回语句中必须相同 如果不同 编译器会报这样的错 基础返回值存在不匹配的类型 对于不透明的 SwiftUI 视图 ViewBuilder DSL 可以改变控制流语句 使每个分支都具有相同的基础返回类型 那么在这个示例中 我们可以通过使用 ViewBuilder DSL 解决这个问题 为方法上添加 @ViewBuilder 注释 同时删除 return 语句将启用 由 ViewBuilder 类型 为我们构建的结果 我们回到 feedAnimal 方法上 我可以在参数列表中用 “some” 因为我不需要 在其他位置的引用不透明类型 在需要多次在函数签名中引用 不透明类型时 这时候名称类型参数就派上用场了 例如 我们把另一个关联类型 “Habitat” 加到动物协议里 我们是希望能够在农场 为某一种动物建立栖息地 (habitat) 在示例里 结果类型取决于特定的动物类型 所以我们需要在参数类型 和返回类型中使用类型参数 A 另一个您经常需要 引用不透明类型的情况是泛型类型 代码通常在泛型类型上声明类型参数 使用存储属性的类型参数 以及在分成员初始化中 在不同的上下文中引用泛型类型 还要求您在尖括号中 显式指定类型参数 声明中的尖括号 可以帮助阐明如何使用泛型类型 所以不透明类型 必须始终为泛型类型命名 现在 让我们构建实现 feed 的方法 我们可以用动物参数的类型 通过 Feed 关联类型 来访问要种植的作物类型 我们将调用 Feed.grow() 来获取作物的实例 作物生产这种类型的饲料 接着 我们需要从 crop 中收获产品 produce 这可以通过调用 作物类型提供的 harvest 方法来完成 最后 我们可以将 produce 喂给动物 因为基础的动物类型是固定的 编译器知道各种方法调用中 植物类型、产品类型 以及动物类型间的关系 这些静态关系防止我们 错误地给动物喂错食物类型 如果我们试图使用没有担保的类型 作为该动物的正确食物类型 编译器会报告我们 了解其他农场协议如何 表达动物饲料类型 与其植物之间的关系 请查看 “在 Swift 中设计协议接口” 讲座 最后 让我们添加一个 喂养所有动物的方法 我将添加一个名为 feedAll 的方法 它能接受一个数组 我知道元素类型需要符合 动物协议 但我希望数组能够存储 不同类型的动物 我们来看 “some Animal” 能否在这帮到我们 有了 “some” 就有一个特定的基础类型 不能改变 因为基础类型是固定好的 数组中的所有元素都要是相同的类型 所以 “some Animal” 数组没能表达正确的含义 因为我想要的是一个可以 容纳不同动物类型的数组 在这里 我们需要一个超类 可以代表任何动物类型的超类 我们可以通过写 “any Animal” 表达任意类型的动物 “any” 关键字表示该类型可以存储 任意动物类型 而且动物的基础类型 在运行时可以有所不同 就像使用 “some” 关键字一样 “any” 关键字也总是 附带一个符合性需求 “any Animal” 是具有以下 能力的单个静态类型 其能够动态存储 任何具体 Animal 类型 这允许我们使用 带有值类型的子类多态 为了允许灵活的存储 “any Animal” 类型在内存中 有特定的代表 您可以把这个代表想成一个盒子 有时 一个值很小 可以直接装进盒子里 而其他比盒子大的值 该值必须分配到别处 同时盒子里存储了指向该值的指针 静态类型 “any Animal” 可以动态存储 任何具体的动物类型 这种类型正式地称作 存在类型 而为不同的具体类型 使用相同代表的策略 称作“类型擦除” 具体类型在编译时会被擦除 而且具体类型仅在运行时才知道 这两个存在类型 “any Animal” 的实例 有着相同的静态类型 但动态类型不同 类型擦除消除了不同动物值 之间的类型级别区别 这允许我们 以相同静态类型 交替地使用不同的动态类型 我们可以用类型擦除 来写一个值类型的 异构数组 这正是我们想要的 feedAll 方法 所以我们把 “any Animal” 数组 作为参数类型 在关联类型协议中 使用 “any” 关键字是 Swift 5.7 中的新功能 要实现 feedAll 方法 我们要先迭代动物数组 对于每种动物 我们想要调用 Animal 协议里的 eat 方法 要调用此方法 我们需要获得本次迭代中 基础动物所对应的特定饲料类型 但是我们尝试 对 any Animal 调用 eat 时 会发现得到了编译器错误提示 因为我们已经消除了特定动物类型 之间的类型级别的区别 我们也同时消除了依赖于 特定动物类型的所有类型关系 包括关联类型 所以 我们无法得知不同动物 想要什么类型的饲料 要依靠类型关系 我们需回到上下文当中 其中特定动物类型是确定的 不直接在 any Animal 上调用 eat 而是调用接受 some Animal 的 feed 方法 这里 any Animal 与 some Animal 是不同类型 但编译器可以将 any Animal 的实例 通过拆解出基础数值 转换为 some Animal 并将其直接传递给 some Animal 参数 这种参数拆解的能力是 Swift 5.7 中的新功能 您可以将拆解当作编译器打开了盒子 并取出存储在里面的值 对于 some Animal 参数的作用域 该值具有确定的基础类型 所以我们可以访问在基础类型上 所有的操作 包括访问关联类型 这真的很厉害 因为它允许我们选择 在需要时灵活存储 同时仍允许我们回到上下文 那里我们有 由确定的基础类型提供的 完整表述的静态类型系统 覆盖函数作用域 而大多数时候 您不必考虑拆解 因为它刚好按您期望的那样工作 类似于在 any Animal 上调用协议方法 确实地调用基础类型的方法 因此 我们可以将每个 动物传递给 feed 方法 在那里我们可以在每次迭代中 种植和收获合适的作物 喂给特定的动物 整个过程中 我们看到了 “some” 和 “any” 有不同的能力 使用 “some” 基础类型是确定的 这允许您在泛型代码中 依靠基础类型的类型关系 这样您就可以在您使用的协议上 访问完整的 API 和相关类型 当您需要存储 任意具体类型时使用 “any” “any” 提供类型擦除 它允许您代表异构集合 代表缺席的基础类型 使用可选性 以及抽象实现细节 一般来说 默认写 “some” 只在您要存储 任意值的时候 将 “some” 更改为 “any” 有了这种方法 在您需要 “any” 只在提供的存储灵活性时 支付类型擦除及其语义限制的成本 这个工作流程类似于默认情况下 使用 let 常量 直到您发觉需要变量的时候 在本次讲座中 我们探讨了 泛化代码的工作流程 随着发展获得的更多功能 我们从编写具体类型入手 随着代码有了更多功能 我们注意到了不同具体 类型之间有重复 然后 我们识别了共同的能力 并使用协议对它们进行泛化 最后 我们用 “some” 和 “any” 来编写了抽象代码 我们确定了倾向于用 “some” 以获得更具表达能力的代码 若想深入研究制定协议 并了解类型擦除 请查看“ 在 Swift 中设计协议接口” 感谢您参与我的讲座 祝您度过美好的 WWDC ♪
-
-
27:10 - Complete example
protocol AnimalFeed { associatedtype CropType: Crop where CropType.Feed == Self static func grow() -> CropType } protocol Crop { associatedtype Feed: AnimalFeed where Feed.CropType == Self func harvest() -> Feed } protocol Animal { associatedtype Feed: AnimalFeed func eat(_ food: Feed) } struct Farm { func feed(_ animal: some Animal) { let crop = type(of: animal).Feed.grow() let produce = crop.harvest() animal.eat(produce) } func feedAll(_ animals: [any Animal]) { for animal in animals { feed(animal) } } } struct Cow: Animal { func eat(_ food: Hay) {} } struct Hay: AnimalFeed { static func grow() -> Alfalfa { Alfalfa() } } struct Alfalfa: Crop { func harvest() -> Hay { Hay() } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。