大多数浏览器和
Developer App 均支持流媒体播放。
-
现代 Swift API 设计
每种编程语言都有一套规范,这是符合人们预期的。通过 SwiftUI、Combine 和 RealityKit 等新 API 中的示例,了解 Swift API 设计中常见的模式。不论您是以团队形式开发 app,还是要发布资源库来供他人使用,都可以了解如何使用 Swift 中的新功能来确保 API 清晰度和正确使用 API。
资源
相关视频
WWDC21
WWDC19
-
下载
大家好
我是 Ben 跟我一起的 是我的同事 Doug 我们将要跟你们讲讲 Swift 里的 API 设计 那么 在有了关于 二进制和模块稳定性的介绍后 我们感到非常兴奋 这是我们第一次能够介绍框架 这个框架很好地利用 Swift 来提供丰富 有效且便于使用的 API 作为 Apple SDK 的一部分 现在 我们已经知道了 设计这些 API 的一部分内容 也就是我们今天要跟你们聊的 那么 我们将要涉及到 一些基本的概念 并且理解它们是如何影响 你的 API 设计的 然后我们需要 深入了解一些 Swift 5.1 的新功能 来看看它们是如何提供帮助 使你的 API 表现更出色 同时我们也将向 你们展示一些例子 源自我们最新的 Swift 框架 包括 SwiftUI 以及 RealityKit
那么 我们之前已经讲过了 API 设计 尤其在 2016 年我们 介绍了 Swift API 设计指南 你们现在仍可以在 Swift.org 网站对这些进行查看 它们包含了一些 很有用的建议 围绕着如何命名 以及为你的 API 写文档 但是我现在不会重复这些 但是有一点 我们今天需要去解决 这就是 使用时的清晰明确 是作为 API 设计师的最大目标 你想要做到这一点 这样一来 在阅读使用你的 API 的代码时 它所做的就会一目了然 同时 你想要做到这一点 使你的 API 可以得到正确使用 好的命名和可读性 对此至关重要 还有一个新的东西 是关于命名的 就是接下来 我们将不在 我们仅限 Swift 使用的 API 中使用 Swift 类型的前缀 现在 这会为 这些 API 带来更整洁 更可读的体验 目前 在 C 和 Objective-C 语言中我们 不得不使用前缀 因为 每个符号都处在全球 命名空间里 没有一个好的方法 来消除歧义
基于这一原因 Apple 和开发人员必须遵循一套 非常严格的前缀惯例 为了统一 我们将 继续使用前缀 在那些 Swift 版本的 API 同时 在 Objective-C 语言有对应的情况下
但是 Swift 的模块系统 可以消除歧义 通过 在类型前面加上 模块名称 由于这个原因 标准库也从来没有前缀 你们中的很多人已经发现了 你们也可以将其从你们的 Swift 框架上去除 但是请记住 即便如此 你们也需要 小心谨慎一点
一个非常笼统的名称会让 你的用户必须手动 区分产生歧义的状况 一旦有冲突的话 而且请始终记住 使用时的清晰明确 某个特定框架的一个笼统名称 可能看上去会有点儿 让人困惑 当你没有上下文的时候 现在 我们要讲几个主题 值 引用 协议和泛型 这之后我们将要讲到 我们的两个新功能 关键路径成员查找和属性包装器 那么 让我们先来讲一下 值和引用 首先快速回顾一下 Swift 有三个创建类型的基本概念 类 结构和枚举
类是引用类型 那代表着当你有一个变量 它就指向 有着实际值的对象 当你对它进行复制 就是在复制引用 那代表着当你 通过引用改变一个值 你实际上在改变 同一个对象 而两个变量都指向这个对象
所以它们都能看到变化 在另一方面 结构和枚举 是值类型 当你复制它们 就会复制其所有内容 那代表着当你 做出一个改变 你只是在 改变那一个拷贝
现在 在你的 API 里使用值类型 能带来很多好处 在使用的清晰明确方面 如果你知道你每次都在 获得一个全新的 独一无二的拷贝 那么你就不需要担心 值来自哪里 是否有人对其设有引用 或者是在 你不知情的情况下 进行修改 你不需要 比如 做一个防御性的拷贝 现在 有了这个 出现了一个常见问题 就是 我是否应该将引用或者 一个值的类型用于我某一段 特定的代码 而每种使用情况是不同的 所以没有一个硬性规定 但是有一些笼统的指南 也就是总的来说 你应该倾向使用 结构而不是类 除非你 有一个使用类的很好的理由 如果你默认使用一个类 当每次在创建类型时 试着将这一默认从你 运行的代码中移开 看看会怎么样
目前 类 在 Swift 中还很重要 它们很关键 如果你需要 通过引用计数 管理资源 虽然 你可能常常想要将 那个类包含在一个结构里 像我们马上会看到的那样
它们也是有用的结构体 如果有东西需要被基础性地 存储和分享 很重要的一点 如果你的类型 具有标识 标识的概念 与值分离 这通常标记着某个类 是合理的
现在 有时我们 必须进行判断 那就是在 RealityKit RealityKit 的 API 围绕着 这些称为实体的东西 这些代表着 出现在场景的对象 它们被存储在 RealityKit 引擎的中心 它们具有标识 当你需要操控一个场景 通过改变对象的 外表或者将它们移动 然后你就直接 在那个引擎里操控对象了 你可以把引用类型看作是 把手 可以处理 RealityKit 中的实际对象 所以这对于引用类型来说 有着完美用途 但是 这些实体的属性 比如它们的定位 或综合某一场景中的方向 被建模成为值的类型 现在 让我们来看看它们 在代码里看上去如何 假设我们想在 这里创建场景 那么我们首先要创建一个 material 类型 然后我们在外面创建 两个盒子 然后我们需要把它们 固定在场景中 接下来 一旦完成这个 我们就可以操控场景 通过直接使用代码我们可以 将小一点盒子在 Y 轴上移动 或者将大一点的盒子旋转 45 度 当我们在 这些引用类型上进行这些操作时 我们正在直接 操控场景 这是很直观的
现在 假设我们想要 像这里一样 也就是 使得每个盒子都是 不同的颜色 我们会使用的一种方法也许是 是将 material.tintColor 设置成红色 现在 我会期待什么结果呢 这个 API 的用户在这个时候 会发生什么呢 这两个盒子都要变吗 因为我改变了同时 创建出这两个的变量 或者只要 后创建的盒子会适用 这一新变化 也就是说 material 应该 作为引用类型还是值类型 这两个中的任一模型都有可能 成为一个 API 的合理设计 虽然 在这里使用 值类型的好处在于 如果 在你的代码里有一段很长的距离 在你一开始 创建和使用 material 类型并进行更改之间 然后你忘记自己 之前使用过它 最后你可能更改了 一部分场景 而你并不想这么做 基于这一理由 RealityKit 选择将 material 作为一个值类型 但是引用语义 对于其他 API 来说也合理 就如我们在实体中看到的一样 重要的一点在于你的 API 有一个容易解释的模型 解释事物如何以及为何运作 最重要的一点是 那一行为如何运作不应该 被类型的偶发 执行细节所驱动 相反 应该是一个 有意识的选择 是基于用例的
那么 我说的 偶发执行细节 是指什么呢 好吧 让我们来看一个 示例类型 比如一个 material 类型 我希望它像一个值 所以我将它设置成一个结构 我赋予它一些简单的属性 比如 roughness 然后我给它一个 textture 属性 让我们假设纹 texture 属性需要去通过 引用计数来管理资源 所以我决定将它设置成一个类
现在 我们之前说过 当你对一个值类型进行复制 你将会复制其所有存储 的属性 但是当你复制一个 引用类型时 你只是在 复制那一个引用 所以当你在复制 这个 material 类型时 最后会发生的情况就是 创建了引用副本 所以两个类型最后 共享了同一个纹理对象
现在 是否可以 实际上取决于 纹理的实现 如果纹理不可变 那么 就非常完美 事实上 这是很理想的 从一个共享的角度考虑
但是如果纹理 在根本上是易变类型 那么我这里所创建的 实际上就有点奇怪了 它的运作既不像 一个引用 也不像一个值 我可以对结构上的属性 做出改变
而且它只影响其中一个变量 但是如果我作出更改 从纹理引用到 对象 那么它将影响两个变量 而这是非常令人惊奇的 对于你 API 的用户来说 也许甚至比 你一直受困于 引用语义还要让人困惑 所以在这儿我们需要做一个 真正的关键区分 在值和引用类型之间 结构与类相比 以及值和引用语义 和类型的表现
仅仅因为一些东西是 值类型 比如结构 未必代表着 你会自动从中获得 值操作 有一种方式 不是 唯一的方式 但是普遍的方式 就是当你将一个可变 的引用类型作为 公共 API 的一部分 所以第一个问题 如果你 想让一些东西表现的像一个值 那么任何 它显示的引用是可变的吗
请记住 这并不 总是很明显 如果我们在处理一个 非最终类 那么你 实际上可能得到的可能是一个可变的子类 幸运的是 我们 有很多技术来避免 类似这样的问题
所以最早的一个就是去做 我们经常针对引用类型 所做的 也就是做一个防御性拷贝 这样我们就可以将 texture 存储属性转为 private
然后创建一个计算后的属性 在设置中 我们复制 texture 对象 这就避免了可变的 子类问题 但是 问题没有解决 如果纹理在 根本上是可变类型 因为你仍然可以改变它 只需要通过 get 方法 这就是引用的工作原理 那么让我们来考虑一下 另一种方式 完全不显示引用类型 相反 只是显示了 我们希望反应在对象上的属性 作为计算生成的属性在我们的 material 值类型上 所以我们可以创建一个通过计算 产生的属性 在 get 方法中 转到相关的 对象的属性 但是在 set 方法中 首先检查对象是否被 唯一引用 如果不是 那么这个时候 我们可以在继续之前 完全复制 texture 对象 并作出更改
通过添加这一行来检查 唯一性 我们已经 执行了完整的写时复制 语义 同时仍显示我们希望出现 在我们的引用类型上的属性
那么接下来 我们来讲讲 协议和泛型 那么 我们已经看过了值类型是如何 使用户清晰明确的 使用你的 API 但是 值类型并不是一个新东西 我们在 Objective-C 语言中一直有 CGPoint 或 CGrect 之类的 那么区别是什么呢 Swift 中的区别 在于可以向 结构和枚举添加协议 同样也包括类 这意味着你可以共享代码 通过使用泛型可以 跨越很多值类型 所以当你觉得你需要 共享一些 不同类型的代码 并不是一定要创建类继承关系 使其中的父类 有共享功能
而是 就像老话说的那样 在 Swift 中 一切从协议开始
但是 那并不意味着当 你打开 XCode 时你会得到 一个空的源文件 你要做的 第一件事是用键盘输入协议 在 Swift API 设计中 就像任何的 Swift 设计一样 首先通过具体的类型探索用例 并且理解 你想要共享的代码是什么 当你发现自己在不同类型上 重复多个功能时 然后通过泛型将代码 共享出去 那么 那可能意味着创建新的协议 但是首先 想一想 通过现存的协议组成 你需要的东西 而且当你在设计 协议时 请确保它们是 可组合的 作为创建协议的 一种替代 你可以考虑 创建一个泛型 那么 让我们来看一些例子 这些例子展示了刚刚提到的 不同的东西 那么 假设我想要创建 一个几何 API 作为其中的一部分 我想要 创建几何向量上的操作 我可能会从为一个 GeometricVector 创建协议开始 我可以赋予其 我想要定义的操作 比如坐标或者 向量间的距离
现在 我需要存储 向量的维度 那么我可能会使我的几何 向量继承自 SIMD 协议 如果你还不熟悉 SIMD 类型 它们 基本上有点像 同质元组 能够很有效地 一次性在每个元素上 执行计算 而且它们还在 Swift 5.1 中有很多了不起的新功能 它们完美地适用于 几何运算 那么 我们要将我们的维度 存储在基础 SIMD 类型中 而且我们还想要对它进行限制 使它只能在标量 SIMD 上工作 这样一来我们就可以 进行我们想要的计算 现在 一旦我们定义了这个协议 我们就可以继续 进行默认的 执行操作 进行所有 我们想在向量上进行的操作 然后我们希望给予 这个协议一致性 针对每一个我们想要其获得 这些新功能的类型 这个三步 定义协议的过程 赋予了它一个默认的执行 然后为多种类型增添了一致性 事实上这有点乏味 我们有必要后退 一步想一想 协议真的是必要的吗 事实上 这些一致性实际上没有它们的 自定义执行 这实际上是一个警告的信号 说明协议是没用的 不是每个类型 都有自定义 而且实际上 这一操作 在每个不同种类的 SIMD 类型上都存在
那么 协议真的有在 带给我们什么东西吗
如果我们退一步 不在我们的新协议上 编写默认执行 相反 我们只是将其编写成 直接在 SIMD 协议上的扩展 有着相同的限制 这样我们就完成了 在这个单页的代码中 我们已经自动将所有 我们需要的功能赋给所有的 包含浮点数的 SIMD 类型
要创建这个协议的 继承式关系 并且将不同类型 划分进继承关系 听上去很吸引人 但是这有点太过于形式 就是让人感觉很满意 但并不总是必要
而且这里总是会涉及到 一个现实的问题 这个没有协议的 更简单的 基于扩展的方式 让编译过程简单很多 如果没有这一串 不必要的协议见证表 你的二进制文件会小很多
事实上 我们发现在 很大的项目上 有着大量的复杂协议类型 通过这个 简化方式并减少 协议的数量 我们可以极大地缩短 编译这些 App 的时间
目前 这个方式 对一小部分项目有用 但是当你在设计 一个更完全的 API 时 就涉及 一个可扩展问题 早些时候 我们想着 创建一个协议 我们说了我们要定义几何向量 并用它继承 SIMD 用于我们的存储 但是这真的是正确的吗 这是一种承继关系吗 我们真的可以说几何 向量是 SIMD 类型吗 我的意思是说 一些操作是合理的 你可以添加或者移除向量 但是有一些不行 你不能将两个 向量相互乘起来 或者将数字 1 添加至向量 但是这些操作在 所有的 SIMD 类型都是可行的 在其他的条件下要有 其他合理的定义 只是不是在几何的情况下
如果我们正在设计一个 便于使用的 API 那么我们也许应该考虑另一个选项 这个选项不是承继关系 而是执行一种组合关系 就是将一个 SIMD 值包含在 一个泛型结构里 这样我们就可以创建一个 几何向量的结构 而且我们将其设定为 SIMD 存储类型的泛型 这样它就可以处理浮点类型 以及任何不同数量的维度 然后 一旦我们完成了这个操作 对于在我们的 新类型上显示什么 API 就有了 更精细的控制 所以我们就可以定义两个 向量的相加 而不是某一向量 单个数字的相加 又或者 我们可以依照标量 定义向量的相乘 而不是两个向量 彼此相乘
而且我们仍然可以使用泛型扩展 这样一来 我们执行的 坐标和距离 就和它们之前的是一样的 现在 我们已经在 标准库使用了这一技术 例如 我们就有了一个 SIMD 协议 而且然后我们有了 泛型结构 其代表了 不同尺寸的 SIMD 类型 请注意 这里并没有 SIMD2 或者 SIMD3 协议 它们不必添加很多值
用户仍可以通过 扩展来为特定尺寸的 SIMD 编写泛型代码 比如 对于一个跨产品操作的 SIMD3 类型 你仅仅想要 定义一个三维的 SIMD 类型 希望这些能让你知道 泛型是如何 像协议一样 强大又具扩展性 现在 我们在这里还是在 借助协议的力量 我们在泛型 SIMD 上 有限制标量类型的浮点 它为我们 提供了基本代码块 可以用于写代码 现在 在我们的 GeometricVector 类型上 我们可以 编写同样的跨产品操作 但是当我们这样做 执行操作看上去 会有点丑陋 因为我们必须保持 间接通过值存储 来获取 X Y 和 Z 坐标 所以 如果我们可以 解决这个问题 会很棒 现在 很显然我们可以 的向量类型上为 X Y 和 Z 编写计算出的属性 但是实际上在 Swift 5.1 有一个新功能叫做 关键路径成员查找 你可以编写单个下标操作 同时显示 一个类型中的多个 不同的计算属性 这样一来 我们就可以根据选择 来使用它 如果可行 就一次性将 SIMD 上的所有属性显示在 我们的几何向量上 让我们来看看我们是如何做的 那么首先 我们将我们的 GeometricVector 标记为 dynamicMemberLookup 属性 紧接着 编译器 会提醒我们写一个特殊的 动态成员下标 这个下标采取关键路径 执行这一下标 的影响是任何 可通过该关键路径 自动访问的属性 都会在我们的 GeometricVector 类型上 显示为计算属性 在这个例子中 我们想要 关键路径是进入 SIMD 存储类型 并返回一个标量 然后我们使用那个关键路径 继续并获取 来自值存储的值并返回 一旦我们完成了这些 我们的几何向量 就会自动获得所有 SIMD 拥有的属性 举个例子 可以获取 X Y 和 Z 坐标 而且它们甚至出现在 Xcode 的自动补全里
如果你想在 Swift 5 里尝试这一功能 当它是基于字符串时 这里的区别在于 这个版本是 完全类型安全的 而且更多的东西是在 编译时完成 既然我们可以访问 X Y 和 Z 属性 那么 我们就可以很大程度上 简化我们的跨产品操作了 就是这样 看上去好多了 目前 这个动态成员 的功能不仅对于 发送属性有用 你还可以将复杂的逻辑放进下标 那么我们再来看一个例子 让我们回到早前的例子 在这个例子中 我们 通过写时复制语义 显示了 texture 的特定属性 这对于一个属性是有效的 但是会很不幸 如果我们需要每次都编写 相同的代码
如果它想要将 texture 上的所有属性 使用写时复制语义属性 作为 material 类型的属性怎么办 那么 我们可以通过动态 成员查找来实现 那么首先 我们要将 dynamicMemberLookup 添加至我们的类型
然后我们执行 下标操作 而且我们要使它采取 一个可写的关键路径 因为我们希望可以同时获取 并设定属性 再将泛型作为返回值 因为我们想要在 texture 中 获取不同的类型 然后我们执行 get 和 set 方法 在 get 方法中我们只需要 进行之前的操作 但是在 set 方法中 在我们做出 更改前 我们要添加唯一 引用检查和 texture 的完整拷贝 通过这样的做法 在这个简洁的下标中 我们显示了 texture 上的每一个属性 同时在我们的 texture 类型上有 写时复制语义 这是一个非常有用的方式 以此来从你的类型中获取值语义
这个新的功能有很多 不同的应用 事实上它和 5.1 中的 一个新功能很好地组合在了一起 那就是属性包装器 接下来 Doug 会聊一聊这个话题 Doug 谢谢你 Ben
那么 Swift 是为了清晰 简洁的代码设计的 也是为了构建表现力优异的 API 对吗 也是为了代码的重新利用 我们已经谈过了 泛型和协议 有了它们你才可以 创建泛型代码 所以为了你的功能和类型 那是可以被再利用的 所以属性包装器 是 Swift 5.1 的新功能 属性包装器背后的想法 是有效地从你编写的 计算属性中获取 代码的重新利用
像这样 这儿有一堆代码
那么这里发生了什么呢 所以 我们尝试去做的就是 显示一个 public 属性 对吗 然后我们得到了这个 我们只是想要一个 image 属性 并且是 public 但是我们并不希望 所有的用户 我们的客户 可以那里编写任意值 我们想要描述一些策略 那么这就变成计算过的属性 我们实际的存储回到 这里 在内部的 imageStorage 属性中 所以通向那存储的访问权限 都是通过 get 和 set 方法建立的 这有很多代码 你们可以先看一看 你们中的一些人也许 认出了这是什么 它真的是一种很冗长的方式 来描述这只是一个惰性变量 image
现在这样就好多了
这是一行代码而不是 有着杂乱访问策略逻辑的 两个属性 我们有这个很好的修饰语 lazy 来告诉你 这里真实的语义是什么
这很重要 能更好的写文档 也更便于阅读 这就是为什么 lazy 自 Swift 1 就在语言中存在了 现在问题是 这是一个更笼统问题的例子 那么 让我们来看看这里的 另一个例子 也就是代码结构 基本上是相似的
但是策略以及实际 在 get 和 set 中 的执行不一样 那么如果你在看 这里的逻辑 你看到这是一个延迟初始化模式 你必须设置过这个 或者在你可以读取之前 曾初始化过 否则你就会失败 这是非常常见的事情
这类的代码出现在 很多地方 我们不会通过 为 Swift 提供另一种语言的扩展 来解决这个问题 但是事实上 我们想要 更宏观地解决这个 因为我们希望人们可以构建 这些东西的库 在这个库中 他们能够分离出 访问单个值的策略 所以这就是 属性包装器背后的想法 就是去消除这种样板 并获得表现更加优异的 API
那么 它们看上去有点像这样 这里的想法就是我们希望 当你在声明一个属性时能 使用标记 所以在这里它就是 public 文本变量 并运用延迟 初始化属性包装器来 给予它特别的语义 来给予它 特别的策略
现在这个 @LateInitialized 这是一个自定义属性 这是一个新的标记 我们在 Swift 中会稍微用到 最根本的 它就是在说 应用这个延迟初始化模式 无论它是什么 我们之后会回到这个来 但是仅仅从代码角度看 这整个都像是惰性的 它为我们带来的好处和 惰性是完全一样的 我们已经完全摆脱了样板文件
但是同时 我们也在 声明的位置就说明了 实际上的语义 比起那些混乱的代码
好的 关于这一行小小的代码 已经讲得够多了 让我们来看看那延迟初始化 实际上看起来是怎么样的 你在这里将会看到 它是一些代码 但是就跟我们刚刚看到的代码一样 延迟初始化背后的 策略模版是一样的
在这里你有 get 和 set set 是更新存储 get 是检查以确保 至少将其设置过一次 并且当我们有设置时 返回值 这非常直接 现在使这个简单的 泛型变得有意思的点在于 它是一个属性包装器 它由这里 顶部的 propertyWrapper 属性 指出是什么样
那么 它所做的就是 允许自定义属性句法 也就是说我们能将这个东西 运用到其他一些属性上 现在 有了 propertyWrapper 属性 就出现了一些要求 主要的一个问题要有 value 属性 这是所有策略执行的地方 所以所有访问 延迟初始化属性的权限都经过这里
而且我们看到实际的 标记是找到一些 提取的概念关于什么是 延迟初始化 在这个特殊的例子中 另一个有趣的事情是 我们已经声明了一个 没有参数的初始化
现在 在属性包装器里 这是可选的 但是当你使用时 它想说明的就是 应用该包装器属性 免费获得了 隐式初始化 在这个特殊的初始化中
好的
让我们来实际运用这个东西 所以 当你通过将其 应用至一个特定的属性 来使用属性包装器时 编译器要将 那个代码翻译成两个分开的属性 基本上 我们正在 扩展成为我们一开始 看到的模式 所以你有一个带 $ 前缀的 备份存储属性 所以 $text 的类型 是属性包装器类型的一个实例 所以我们现在有一个 LateInitialized 字符串
它提供存储
通过调用无参数初始化 它将由编译器进行隐式初始化 就是我们刚刚提到的 因为它在那儿 现在我们 获得了隐式初始化 而延迟初始化随时可以进行 你也许能想起来 存储被设成了 nil
编译器在这里 还做了一件事 就是 将文本翻译成计算属性 所以 get 要 创建一个对 $text 的访问 然后通过 调用 get 值来获取 $text 的值 在 set 里我们也进行同样的操作 在 $text.value 中写下新的值 所以就是这个 属性包装器能有 其自己的存储 无论它想要怎么存储 无论是本地或者其他地方 然后无论执行什么 你指定的策略 通过 get 和 set 访问数据
所以总的来说 这是很好的 我们已经将策略和它的应用分离 将其放在 延迟初始化包装器中 无论我们想要什么类型 我们都可以对任意数量的 不同属性进行操作 在简单很多同时有更少的样板文件
那么 让我们来看看另一个例子 Ben 有提到过关于 值和引用语义的东西 现在 当你在处理 引用语义和可变状态时 你会发现在 在某些时候进行着 防御性拷贝 当然 我们可以手动在任何 需要的地方做这件事 但是为什么不为此 构建一个属性包装器呢 所以 这就是属性包装器 它的样子基本上 和我们之前看到的一样 它有存储能力 它有一个值属性 这个属性包装器的 所有策略都在 set里 当我们有了一个新的值 继续并复制这个值 而且由于我们正在使用 NSCopying 来进行复制 它就会继续并调用复制方法 然后进行投射
关于防御性拷贝的 另一件有趣的事情在于 它提供一个初始值的初始化 这就像那个没有参数的初始化 在你的属性包装器里 它并不是必须的 但是如果它在那里 就会让你在属性包装其中的 所有属性 都被赋为默认值 那个默认值会被填入这个初始化 我们可以在 初始化上实行任何我们 希望的策略 在我们的这个例子中 就跟 set 策略一样 我们想要继续并创建一个 防御性拷贝
之后为它分配一个结果 让我们来看一下 那么 如果我们要定义一些 防御性拷贝 UIBezierPath 会发生什么呢 好吧 编译器会 将其编译成两个 不同的定义 首先 在这里我们将会 有一个初始化 所以这里 无论何时我们创建这个路径 它有一个默认的 UIBezierPath 只是创建了空白的实例 然后当我们继续并 将其变成多个属性时 就有了备份 存储属性 $path 请注意我们是如何将它初始化的 我们将用户提供的 初始值传输至 初始值初始化 这样它 就可以被防御性复制了 get 和 set 方法 看上去完全一样 get 和 set 都需要 $path.value
现在 当然在默认情况下 是正确的语义 防御性拷贝应该 继续并进行复制 在这个例子中我们了解了 有关默认值的知识 就是创建一个新的对象 为什么我们要操作并复制呢 所以 让我们来稍微优化一下 因为我们可以做到
所以我们能够扩展防御拷贝 它只是一个类型 关于这个类型 没有什么神奇的 除了在使用时它表现得像 一个属性包装器 而且我们可以添加 withoutCopying 初始化 当我们这么使用时 我们可以进行并为我们的类型 编写一个初始化程序
在这里我们在做的就是 我们正在为 $path 赋值 让它调用防御性 withoutCopying 初始化程序 这样我们就可以避免 出现多余的拷贝
所以这里没有更多 神奇的事情了 $path 只是一个存储属性 它唯一神奇的点在于 它是编译器作为将其应用到 属性包装器模式的一部分 而生成的 但是你可以 将它作为一个普通变量对待 包括设定 自己的初始化 现在 这还是 有点像样板文件 很不幸的是 我们不得不 编写那个初始化程序 所以你要看到 其他的初始化格式了 当你在
使用 @DefensiveCopying 你可以就在属性声明里 进行初始化 按照你的想法来 所以这里我们就可以继续并调用 withoutCopying 初始化程序 来用我们想要的值 初始化备份存储属性 这个一个很好的小声明 而你还是获得了在 记忆初始化 获取默认设定的好处
好吧 所以 属性包装器事实上 还是很强大的 它们将访问数据的 这个策略概念抽象化了 所以你可以决定如何存储你的数据 而且你也可以决定如何访问你的数据 针对你的属性包装器 你的用户要做的就是 使用那个自定义属性句法 来联系进你的系统 所以在我们开发 属性包装器时 我们发现了 针对它们的很多不同使用 都是围绕这类数据访问 的大致概念的 所以举个例子 你也许已经 看到用户默认示例 这里就是我们 建立的一个关系 这个关系在 你刚刚作为 Bool 引用的 Swift 中类型良好的属性 和一些字符串 类型实体之间 所以我们在结构参数中 明确描述了 如何继续并获取数据 处理用户默认的 所有逻辑 就在这个 用户默认属性包装器中完成了 所以我们构建 @ThreadSpecific 如果你想要以本地线程存储 你可以应用 ThreadSpecific 属性包装器 处理系统的线程相关 存储的所有细节 就都在属性包装器里面 你可以就把这个东西想成 一个本地的记忆池 还有 在 Swift 社区 正如我们已经 构建的这个功能 我们发现 在描述命令行参数时 它实现得非常好 如果你通过一个 库构建了一个命令行工具 用于记录 简写语法 意思是我想要 Minimum value 这就是 @Option 的描述 这就是用户传递的 缩写字符串 以及含义是什么 以及所有其他的东西 你需要非常非常简洁地 说明你的命令行选项 有很多真的非常酷 的东西我们可以通过 属性包装器完成 因为这个良好 干净的句法 可以生产一些东西
也许你在 另一场会议中已经了解了 我们在 SwiftUI 中广泛地 使用了属性包装器来描述 视图的数据依赖
在 Swift UI 中 有几个不同的 属性包装器存在 那么这里我们有一个 @State 来引入查看本地状态 对于一些高级引用 我们有 @Binding 来 声明它是来自其他地方的 你也许已经看到了 @Environment 一个环境对象
所有的这些都是属性包装器 在 Swift 中以那种方式 描述它们的一个好处在于 你在明确你的策略 数据在哪里 它是如何被访问的 都可以在声明中给出 但是当你继续并构建你的视图时 你并不在意那个 你不在乎数据在哪里 系统为你管理好了 你就可以查看 Keynote 讲演的 特定的一页 输入数字 如果你想要继续并编辑 一些东西 那么你可以使 找到实际捆绑的幻灯片
所以 处理数据的 所有逻辑 以及观察 变化更新 或者甚至是 在存储数据 在属性包装器类型中 的策略里都解决了 你不需要考虑 你只需要考虑处理 你实际的数据 现在 在这页幻灯片上 有一个东西有点意思 那么这是 $slide.title 我们将其传递 这样就可以 在文本框编辑页标题
$slide 我们之前已经看到过了 那是备份存储属性 那就是编译器 为我们进行合成 因为我们在这里应用了 Binding 属性包装器
但在 Binding 中 是没有标题的
标题是我的数据模型的一部分 我一个幻灯片的数据模型 那么这给了我们什么呢 所以 实际上这是一个 属性包装器和 先前 Ben 讲到的关键路径成员 查找功能的结合 那么这样的话 让我们实际来 关注 @Binding 也就是在 Swift UI 里的东西 并且提供了 这个高级引用 首先也是最重要的 Binding 是一个属性包装器 所以它有 value 可以以任何类型的参数存在 因为你可以对任何东西进行捆绑 它有着任何样式的访问权限 这并不重要 我们并不知道这是什么 因为是由框架为我们处理的 通过泛型下标 捆绑也是支持关键 路径成员查找的泛型下标 有点儿拗口 我们不需要知道 是怎么实现的 但是我们应该更加仔细地 看一看类型签名 因为它很有趣 那么 我们已经讲过了关键路径 它与特定的值类型紧密联系 所以我们在进行捆绑的东西 比如幻灯片页 以及在那个特定实体里访问任何属性 这里返回的不是一个值 不是引用一些东西 我们返回的是一个新的捆绑 它的焦点在于外部捆绑的 某一个特定的属性 仍然保持着数据依赖
那么 在实际操作中这是怎么样的呢 好吧 我们有 slide 捆绑在我们的 Slide 类型上 基本上 我们将其 视作是一个值 所以我们可以引用 slide 我们得到了一个 slide 实例 我们可以引用 slide.title 并且 获得那个字符串的实例 同时在后台 追踪所有修改 如果我们输入 $Slide 好的 我们将那个捆绑的实例放到了 slide 中 当我们输入 $slide.title 好的 现在我们在查找一个属性 它并不在捆绑中 所以编译器将其进行了重写 将其变成使用动态数字下标 将一个关键路径 放进 slide.title
这解析的方式是 将一个焦点捆绑点 变成字符串属性 它位于之前的捆绑中 并且遵循所有的 数据依赖 那么 如果我们抛开 之前我们一直在关注的 语言机制然后 看一看我们在这里进行的 高级代码 会非常好 在属性包装器里 使用这个自定义属性 我们建立了数据依赖 我们有访问我们数据的 基础权限 很容易读取 或者进行修改 如果我们想要通过 我们的捆绑传递一个 高级引用 我们把这个前缀 $ 放在它的前面 我们获得的影响是 我们总是可以向 一些其他视图传递一个捆绑
那么 我们已经讲到了一些 不同的主题 我们讲到了值语义 和引用语义 何时去使用两者以及如何 让它们协作运行
我们也谈到了 泛型和协议的使用 请记住 协议是极其强大的 但是用它们来进行代码重用 这就是它们的作用 而不是用来分类或者 构建庞大的继承结构 因为它们会阻碍你 你不需要这么做 最后 我们深入讲了讲 属性包装器语言功能 以及如何用它来 抽象访问数据
好的 非常感谢你们 如果你想要就任何这些话题进行讨论 欢迎来我们的实验室
[掌声]
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。