大多数浏览器和
Developer App 均支持流媒体播放。
-
Swift 的新功能
与我们一起来了解 Swift 的更新。我们将向你展示 API 如何通过参数包和宏等功能变得更具可扩展性和表现力。我们还将带你了解互操作性方面的改进,并分享我们如何将 Swift 在性能和安全方面的优势扩展到各个领域 - 从 Foundation 到服务器上的大规模分布式程序。
章节
- 0:39 - Swift project update
- 2:44 - Using if/else and switch statements as expressions
- 3:52 - Result builders
- 4:53 - type parameter packs
- 9:34 - Swift macros
- 19:47 - Swift foundation
- 23:25 - Ownership
- 27:59 - C++ interoperability
- 32:41 - What's new in Swift Concurrency
- 38:20 - FoundationDB: A case study
资源
相关视频
WWDC23
WWDC21
-
下载
♪ ♪
Ben:大家好 欢迎收看 “Swift 5.9 中的新功能” 我是 Ben 我和我的同事 Doug 将带你了解今年 Swift 语言的一些改进 我们将介绍一些使用 Swift 简洁语法 更简便地表达意图的方法 一些帮助框架作者 使他们的新 API 使用起来 更自然的强大新功能 我们还将介绍一些 新方法来更好地控制 底层代码的性能和安全性 我们先来介绍一下 Swift 开源项目 这是 Swift 很棒的更新 如果没有 Swift 社区 和集结在 swift.org 的语言贡献者和使用者 共同努力发展该语言 并支持新的创意 这一切就不可能实现 Swift 遵循开放的语言进化过程 新功能或重要行为改变都会在 Swift 论坛上公开提出和审查 如果你想继续学习 可以在 Swift 网站上 找到所有语言提案的布告栏 一年前 我们见证了 Swift 项目治理的 重大重组 核心团队宣布成立语言指导小组 主要负责监督 Swift 语言 和标准库的发展 从那之后 语言指导小组监督了 40 项新语言提案 我们今天会提到其中一部分
但有时 单个语言提案 会作为一个更广泛主题的 一部分聚集在一起 例如 Swift 并发的附加功能 这是通过十项单独的提案的提出的 为了应对这样的案例 语言指导小组 提出了通过构想文档将这些提案 联系在一起的新方法 这些文档为更大型的语言修改做出规划提案 语言指导小组最早接受的提案 是 Swift 宏的构想 Swift 5.9 的一项新功能 我们会在本次演讲的后面谈到 当然 语言的进化只是 Swift 社区工作的一部分 一门成功的语言需要的远不止这些 它需要出色的工具、 对多平台的强大支持 和丰富的文档 为了监督这一领域的 进展 核心团队将组建 一个与语言指导小组 并行的生态系统指导小组 这个新组织是最近在 swift.org 的一篇博客文章中提出的 我们期待着 关于这个新小组成立的进一步公告 现在 我们来谈谈今年 Swift 语言的变化 从在代码中以更好的方式 表达自己开始 Swift 5.9 包含了 可能是我们呼声最高的语言增强功能 允许将 if/else 和 switch 语句用作表达式 从而提供了一种整理代码的好方法
例如 如果你想要在一些复杂的条件下 初始化一个 let 变量 你必须借助技巧 比如这个难以阅读的复合三元表达式
If 表达式允许你改用 更熟悉和可读性高的 if 语句链
另一个有用的地方 是初始化全局变量或存储属性 这里单独的表达式运行正常 如果你想添加一个条件 你必须把它放在一个闭包中 之后会被立即执行
既然 if 语句可以是表达式 你可以丢掉那些杂乱的内容 留下更整洁的代码 结果构造器 即驱动 SwiftUI 等功能的声明性语法 在今年进行了显著的改进 包括优化类型检查性能、 代码补全和改进错误信息
这种改进尤其针对无效代码 之前 有错误的结果构造器代码 无法马上报错 因为类型检查器会探索 许多可能的无效路径
从 Swift 5.8 开始 无效代码类型检查要快得多 无效代码上的错误信息更加精确 例如之前 某些无效代码 可能会在结果构造器 完全不同的部分中引发误导性错误 在 Swift 5.7 中 当错误实际发生的时候 你就会收到一个这样的错误
在最新版本中 你会收到 识别真正问题的更准确的编译器诊断 接下来 我们谈一谈对泛型系统的补充 会如何对你每天使用的 框架产生一些重大改进
几乎所有你写的 Swift 代码都使用了泛型 类型推断允许使用这些类型 而无需了解构建它们的高级功能 例如 标准库数组类型使用泛型 来提供一个数组 可以与你可能想要存储的 任何类型的数据一起工作 当你需要使用数组时 你需要做的就是提供元素 不需要为元素类型指定显式参数 因为它可以从元素值中推断出来
Swift 的泛型系统 通过保留类型信息 实现自然的 API 这样你的代码就可以无缝在 你提供的具体类型上进行操作 这里是一个受 Swift 编译器 自身代码库启发的例子: 一个接受请求类型的 API 对其进行评估以产生一个强类型的值 所以你可以请求一个布尔值 并得到一个布尔值的结果
现在 一些 API 不仅想抽象具体类型 而且还想对你传入的 参数数量进行抽象 因此 一个函数可能接受 一个请求并返回一个结果 或者接受两个请求 并返回两个结果 或者更多
为了支持这一点 泛型系统必须 与处理多个参数长度的机制一起使用 这样你传入的所有类型 都与你传出的类型相关联 在 Swift 5.9 之前 完成这种模式的唯一方法 是为 API 支持的每个特定参数长度 重新定义一遍 但这种方法有局限性 它对你能传递的参数数量 强加了一个人为的上限 如果你传递的参数太多 就会导致编译器错误 在这个例子中 所有重载都不能处理 超过 6 个参数 但我们却传了 7 个 这种重复定义的规律和 它自身的限制的问题 在理论上需要处理无限参数长度 的 API 中普遍存在
在 Swift 5.9 中 泛型系统 通过对参数长度进行泛型抽象 获得对该 API 模式的直接支持 这是通过一个新的语言概念来实现的 它可以表示“打包”在一起的 多个独立的类型参数 这个新概念被称为类型参数聚合 使用参数聚合 当前对每个固定参数长度 有单独重载的 API 可以被压缩成单一的函数
evaluate 函数现在不是接受 一个参数和与这个请求 对应的 Result 类型 而是接受任何与各自 Result 类型对应 的独立请求 该函数会在括号中返回每个 Result 实例 括号中会是单一的值 或是包含每个值的元组 evaluate 函数现在可以处理 所有参数的长度 没有人为限制
类型推理让使用参数聚合的 API 可以很自然地使用 而不需要知道 API 正在使用聚合
调用我们的新 evaluate 函数 现在可以处理任何数量的参数 看起来就像调用固定长度的重载 Swift 会根据你调用函数的方式 来推断每个参数的类型和总数 想了解如何编写 像这样的泛型库 API 请查看使用参数聚合的泛型化 API 以一种自然的方式调用泛型 API 展示了 Swift 的基本设计目标之一 即通过简洁的代码进行清晰的表达
Swift 的高级语言功能使赏心悦目的 API 更容易表达你的意图
无论是通过数组或字典使用泛型 还是在 SwiftUI 中设计 UI 从你在 Swift 写第一行代码开始 这些高级语言功能都会有所帮助 Swift 接受渐进式展开意味着 你可以准备好了 再学习更高级的功能
Swift 5.9 将这种设计方法 提升到了一个新的水平 它为库作者提供了一个新的工具箱 可以使用新的宏系统 进行表达式 API 设计 有请 Doug 来为大家介绍更多内容 Doug:使用宏 你可以扩展语言本身的功能 消除重复代码并解锁 Swift 更多表达能力 让我们想一下一直存在的 assert 函数 它用来检查一个条件是否为真 如果条件为假 assert 函数会停止程序 但当这种情况发生时 你几乎得不到 任何出错的信息 只有文件和行号 你需要增加一些日志记录或 在调试器中捕获程序以进一步了解 已经有人试图改进这一点了 XCTest 提供了一个 assert-equal 操作 它可以分别获取两个值 所以运行失败后 你至少 可以看到两个不相等的值 但是我们仍然不知道哪个值是错的 是 a、b 还是 max 的值? 这种方法不适用于我们在断言中 执行的各种类型的检查 如果我们回到最初的断言 当我们的断言失败时 在源代码中有很多信息 我们希望在记录中看到 这段代码是什么? a、b、c 的值是什么? max 产生了什么? 除非通过一些自定义的功能 我们以前无法在 Swift 中改进这一点 但宏实现了这一点
在这个例子中 “#assert” 语法 正在展开名为 “assert” 的宏 # 语法可能看起来很熟悉 因为 Swift 已经有一些同样拼写的内容 比如 #file、 #selector 和 #warning assert 宏看起来和感觉上 都和函数版本一样 但因为它是一个宏 所以当断言失败时 它可以提供更丰富的体验 现在程序显示的是失败断言的代码 以及导致结果的每个值
在 Swift 中 宏是 API 就像类型或函数一样 你可以通过导入 定义它们的模块来访问它们 和许多其他 API 一样 宏是以包的形式分发的 这里的 assert 宏来自 power asserts 库 它是一个开源的 Swift 包 可以在 GitHub 上找到
如果你要查看宏包 你会发现 assert 的宏声明 它是用 “macro” 关键字引入的 但除此之外 它看起来很像一个函数 有一个未标记的 Bool 参数 用于检查条件 如果这个宏产生一个值 这个结果类型将用 通常的箭头语法来写 对该宏的使用将根据参数 进行类型检查 这意味着 如果你在使用宏的过程中 犯了错误 比如忘记将最大值 与某些内容进行比较 在宏被展开之前 你会立即得到 一个有用的错误信息 这使得 Swift 在使用宏时 提供了很好的开发体验 因为宏可以 在类型正确的输入上操作 并产生代码 以可预测的方式 增强你的程序 大多数宏被定义为 “外部宏” 通过字符串指定宏实现的模块和类型 外部宏类型是在作为编译器插件的 独立程序中定义的 Swift 编译器将使用宏的源代码 传递给插件 该插件会产生新的源代码 然后它会被整合回 Swift 程序中 在这里 宏正在将断言扩展为代码 以捕捉各个数值 以及它们在源代码中应该显示的位置 你不会想自己写这些模板 不过宏会为你做这些 宏声明有一个额外的信息 即它们的角色 这里的断言宏是一个 freestanding 的表达式宏 它被称为 freestanding 因为它使用了 “#” 语法 并可以直接在语法上 操作以产生新代码 它是一个表达式宏 因为它可以在任何 可以产生值的地方使用 新的 Foundation Predicate API 提供了一个表达式宏的好例子 谓词宏让人们使用闭包 以类型安全的方式编写断言 产生的谓词值可以用于一些其他 API 包括 Swift 集合操作 SwiftUI 和 SwiftData 该宏本身在输入类型的集合上 是泛型的 它接受一个闭包参数 作为函数 处理这些 输入参数 并产生 一个布尔结果 输入集合是否匹配? 宏会返回一个新的 Predicate 类型的实例 它可以用在程序的 其他地方
不过 宏还有更多的应用 因为我们最终写的很多重复代码 都是因为我们需要为已有代码 派生出衍生代码 举个例子 我发现我在自己的 代码中经常使用枚举 比如这个可以捕获相对 或绝对路径的路径枚举 但是我经常发现自己 需要检查一个特定的情况 比如 从一个集合中过滤所有绝对路径 当然 我可以把这个 isAbsolute 检查写成一个计算属性 但我迟早还得再写一个 这有些无聊
宏可以帮我们生成这些重复代码 CaseDetection 是一个附加宏 使用与属性包装器相同的 自定义属性语法编写 附加宏将其应用的 声明的语法作为输入…… 这里是枚举声明本身……并将生成新代码
这个宏扩展的代码是正常的 Swift 代码 编译器会将其整合到你的程序中 你可以在编辑器中检查 宏生成的代码 对其进行调试 如果你想进一步自定义它 请将其复制出来 等等
附加宏根据其对所附声明的增强方式 被分为五种不同的角色 我们刚才讨论的 CaseDetection 宏 是一个“member”附加宏 意味着它会在类型 或扩展中创建新的成员 Peer 宏会在它们所附声明 旁边添加新的声明 例如 为 completion-handler 版本 创建一个 async 方法 反之亦然
Accessor 宏可以将一个存储的 属性变成一个计算的属性 它可以用来对属性访问执行特定操作 或者以类似于属性包装器 但比其更灵活的方式抽象出实际存储 附加宏可以将属性 引入到一个类型的特定成员上 以及添加新的协议符合性 几个附加宏角色可以组合在一起 以达到一些有用的效果 这方面的一个重要例子是观察 观察一直是 SwiftUI 的一个组成部分 为了能够观察一类属性变化 只需要让类型符合 ObservableObject 并将每个属性标记为 @Published 然后在你的视图中使用 ObservedObject 属性包装器 这包含很多步骤 错过一个 可能意味着 UI 不能按预期更新 我们可以利用基于宏的观察做得更好
将 Observable 宏附加到一个类上 可以对其所有的存储属性进行观察 不需要对每个存储的属性进行标注 也不需要担心 如果不这样做会发生什么 因为 Observable 宏会处理这一切
Observable 宏组合了三个宏角色 让我们深入了解 这些角色是如何一起工作的 每个宏的角色都对应于 Person 类 被 Observable 宏增强的某种特定方式 member 角色引入了新的属性和方法
member 属性角色会把 @ObservationTracked 宏 添加到被观察类的存储属性中 这又扩展 getter 和 setter 以触发观察事件的 最后 conformance 角色会引入 与 Observable 协议的一致性
看起来代码很多 但这都是普通的 Swift 代码 它们都整齐地折叠在 Observable 宏后面
每当你需要查看任何宏是如何扩展的 以更好地了解它对你的程序的影响 它就在 Xcode 中触手可及
点击使用 “Expand Macro” 可以在你的编辑器中 看到宏扩展后的源代码 宏生成的代码中的任何错误信息 都会自动显示扩展后的代码 你可以用你的调试器单步进入和退出它
Swift 宏提供了一种新的工具 可以实现更具表现力的 API 消除 Swift 中的重复代码 帮助解锁 Swift 的表现力 宏对其输入进行类型检查 产生常规的 Swift 代码 并在你的程序中的定义点实现整合 因此它们的效果很容易推断 而且 如果你需要了解一个宏的作用 可将其源代码扩展在你的编辑器中 我们只是介绍了宏的一点知识 “扩展 Swift 宏” 将 深入探讨 Swift 宏的设计 回答你所有想问的问题 你还可以通过 “编写 Swift 宏” 动手实现你自己的宏 我迫不及待想看到 Swift 社区中出现的新的宏了
Ben:从一开始 Swift 就被 设计成一种可扩展的语言 Swift 的设计强调表现力 代码清晰简洁 不讲究仪式 易于阅读和编写 利用 Swift 的强大功能 如泛型和原生并发支持 SwiftUI 或 SwiftData 等框架 让你快速实现你想要的结果 有更多时间专注于重要的事情
尽管有这些高水平的功能 Swift 依旧很高效 它是原生编译的 它使用值类型 和引用计数而不是垃圾收集 这意味着它能够实现低内存占用
这种可扩展性意味着我们能够使 Swift 触达比以前使用 Objective-C 时更多的地方 到之前你认为必须使用 C 或 C++ 的 底层系统 这意味着将 Swift 更清晰的代码和关键的安全保证 应用到更多地方 我们最近开放了通过 Swift 重写 Foundation 框架的源代码 这一举措将使得 Foundation 在 Apple 和非 Apple 平台上 共享同一实现 但这也意味通过 Swift 重写大量的 Objective-C 和 C 代码 截至 MacOS Sonoma 和 iOS 17 有全新的 Swift 支持的 基本类型的实现 如 Date 和 Calendar 有格式化和国际化的基本类型的实现 如 Locale 和 AttributedString 以及 JSON 编码和 解码的全新的 Swift 实现 性能方面的优势也很明显
Calendar 计算重要日期的能力 可以更好地利用 Swift 的值语义来避免临时分配 从而在一些基准测试中提高了 20% 以上 FormatStyle 的日期格式化 也获得了一些重大的性能升级 在使用标准日期和时间模板 进行格式化的测评中 获得了 150% 的巨大提升 更令人兴奋的是 新软件包中对 JSON 解码的改进 Foundation 为 JSONDecoder 和 JSONEncoder 提供了一个全新的 Swift 实现 消除了与 Objective-C 集合 类型之间高代价的往返操作 通过与 Swift 的 JSON 解析的紧密整合 Codable 类型初始化 也提高了性能 在解析测试数据的测评中 新的实现要快 2 到 5 倍 这些改进既有减少从旧的 Objective-C 实现到 Swift 的桥接成本 也有新 Swift 实现更快的速度
让我们以一个测评为例来看看 在 Ventura 中 由于桥接成本 从 Objective-C 中 调用 enumerateDates 比从 Swift 中调用稍快 在 macOS Sonoma 中 从 Swift 中调用同样的功能要快 20% 部分速度的提高 是因为消除了桥接成本 但新函数的实现本身也更快 从 Objective-C 中调用它时就可以看出 这个特定的日期计算并不太复杂 所以这是减少两种语言之间 开销的好方法 有时候 当你做系统底层操作时 需要更精细的控制 来达到必要的性能水平 Swift 5.9 引入了一些新的功能选项 帮助你实现这种程度的控制 这些功能侧重于所有权概念 也就是说 当一个值在你的 App 中传递时 代码的哪一部分“拥有”它
为了了解你什么时候 可能会使用这些功能 我们先来看一些示例代码 这里是一个非常简单的 文件描述符的包装器 它可以让我们给底层的系统调用 一个更好的 Swift 接口 但是在这个 API 上 还是有一些容易犯错的地方 例如 你可能会在调用 close 后尝试写入文件 而且你必须小心 在对象超出作用域之前 总是通过调用 close 手动关闭它 否则就会导致资源泄漏 解决方案之一是把它变成 一个带有 deinit 的类 当类型离开作用域时自动关闭它
但这仍然有别的问题 比如要进行额外的内存分配 这通常不是一个大问题 除非是在一些非常受限的系统环境中
类也有引用语义 你可能会无意中在各线程间 共享一个文件描述符类型 导致竞赛条件 或者无意中存储它
不过 让我们回头看看结构版本
真的 这个结构也像一个引用类型 它包含一个引用真值的整数 而真值是一个打开的文件 对这种类型进行复制 也可能导致在你的 App 中 无意地共享可变的状态 从而导致 bug 你需要的是抑制该结构复制的能力
Swift 类型 无论是结构 还是类 默认都是可拷贝的 在大多数情况下是正确的选择 虽然过多的非必要拷贝 有时会成为你代码中的瓶颈 但偶尔花时间在工具中 找到这些瓶颈 总比经常因为编译器 要求你明确这些拷贝而被困扰要好 但有时这种隐式拷贝 并不是你想要的 特别是在 对一个值进行拷贝可能 导致正确性问题的时候 比如我们的文件描述符包装器 在 Swift 5.9 中 你可以 用这种新的语法来操作 这种语法可以应用于 结构和枚举的声明 它可以抑制隐式拷贝类型的能力 如果某个类型是不可复制的 你就可以给它添加一个 deinit 就像可以为类所做的一样 当该类型的一个值 超出作用域时 它会运行
不可复制类型也可以 用来解决调用 close 后 仍然调用其他方法的问题
close 操作可以被标记为消耗性 (consuming) 的 调用一个消耗性的方法 或参数会将一个值的 所有权交还给所调用的方法 由于我们的类型是不可复制的 放弃所有权意味着 你不能再使用这个值
默认情况下 Swift 中的方法 会借用其参数 包括其自身 所以你可以调用 write 方法 它借用了文件描述符 用它来写出缓冲区之后 值的所有权就回到了调用者那里 然后你就可以调用 另一个方法 比如 close
但是由于 close 已经被标记为消耗性的 而不是默认的借用 所以那必须是它的最终用途
这意味着 如果你先关闭文件 然后试图调用另一个方法 比如 write 你会在编译时 得到一个错误信息 而不是运行时失败 编译器还会指出 消耗性使用发生在哪里
不可复制类型是 Swift 中系统级编程的 一个强大的新功能 它们仍然处于发展的早期阶段 以后的 Swift 版本将不可复制类型 扩展到泛型代码中 如果你有兴趣关注这项工作 Swift 论坛上正在积极开展讨论 Doug:Swift 成功的一个关键是 它与 Objective-C 的互操作性 从一开始 开发者就能 在他们现有的代码库中逐步采用 Swift 一次只混合一个文件或模块 但我们知道很多人 并不只是用 Objective-C 写代码 许多 App 还具有 以 C++ 实现的核心业务逻辑 与之对接并不那么容易 通常这意味着要增加一个 额外的人工桥接层 从 Swift 到 Objective-C 再到 C++ 然后一路返回 Swift 5.9 引入了 Swift 与 C++ 类型和函数直接的互动能力 C++ 互操作性的工作方式与 Objective-C 互操作性的工作方式一样 将 C++ API 映射到 你可以直接从 Swift 代码中 使用的 Swift 对应项 C++ 是一种大型语言 它有自己的概念 比如类、方法和容器等等 Swift 编译器可以理解常见的 C++ 习语 所以许多类型可以直接使用 例如 这个 Person 类型 定义了一个 C++ 值类型所期望的 五个特殊成员函数: 复制和移动构造函数、 赋值运算符和析构函数 Swift 编译器将其视为一个值类型 并会在正确的时间 自动调用正确的特定成员函数 此外 像 vector 和 map 这样的 C++ 容器 也可以作为 Swift 集合访问
所有这些的结果是 我们可以编写 直接使用 C++ 函数 和类型的简洁的 Swift 代码 我们可以在 Person 实例 的 vector 上进行过滤 调用 C++ 成员函数并直接访问数据成员
在另一个方向 从 C++ 中 使用 Swift 代码的机制 和在 Objective-C 中 使用 Swift 代码的机制一样 Swift 编译器会产生一个“生成的头文件” 其中有包含 C++ 视图的 Swift API 不过 与 Objective-C 不同的是 你不需要限制自己 只使用用 objc 属性标注的 Swift 类 C++ 可以直接使用大多数 Swift 类型及其完整的 API 包括属性、方法和初始化定式 不需要任何桥接开销 这里我们可以看到 C++ 是 如何利用我们的 Point 结构的 在包含生成的头文件之后 C++ 可以调用 Swift 初始化方法 来创建 Point 实例 调用 mutating 方法 并访问存储和计算的属性 所有这些都不需要 对 Swift 代码本身做任何改动
Swift 的 C++ 互操作性 使得整合 Swift 与现有 C++ 代码库 变得前所未有的简单 许多 C++ 习语可以直接在 Swift 中表达 通常会自动表达 但偶尔也需要一些标注 来表明所需的语义 Swift API 可以直接从 C++ 中访问 不需要标注或改变代码 这使得在使用任何 C、C++ 和 Objective-C 混合的代码库中 逐步采用 Swift 成为可能
C++ 互操作性是一个不断发展的过程 由 C++ 互操作性工作组指导 想要了解更多信息 请查看 演讲“混合 Swift 和 C++” 或加入我们在 Swift 论坛上的讨论
语言层面的互操作性确实很重要 但你也必须能够构建你的代码 而要用 Xcode 或 Swift Package Manager 取代你现有的构建系统 才能开始使用 Swift 这和重写大量代码 一样 都是很大的障碍 所以我们与 CMake 社区合作 在 CMake 中改进对 Swift 的支持 你可以通过声明 Swift 是项目的语言之一 并将 Swift 文件放入目标中 将 Swift 代码整合到你的 CMake 构建中
更重要的是 你可以在一个 目标中混合使用 C++ 和 Swift 而 CMake 将确保分别编译这两种语言 并为这两种语言链接 所有适当的支持库 和运行时 这意味着你今天就可以 开始在你的跨平台 C++ 项目中 逐个文件或逐个目标的采用 Swift 我们还提供了一个 包含 CMake 项目的示例库 其中包含 Swift 和 C++/Swift 混合目标 包括使用桥接和生成的头文件 以帮助你入门
几年前 我们在 Swift 中 引入了一个新的并发模型 该模型基于 async/await、 结构化并发和 actor 等构建模块 Swift 的并发模型是一个抽象模型 可以适应不同的环境和库 这个抽象模型有两个 主要部分:task 和 actor task 代表一个顺序工作的单位 在概念上可以在任何地方运行 只要程序中有 “await” task 就可以暂停 task 能够继续后就可以恢复运行
actor 是一种同步机制 提供对隔离状态的互斥访问 从外部进入一个 actor 需要一个“await” 因为它可能会暂停 task
task 和 actor 被整合到抽象语言模型中 但在该模型中 它们 可以用不同的方式实现 以适应不同的环境 task 是在全局并发池上执行的 全局并发池如何决定 安排工作 取决于环境 对于 Apple 的平台 Dispatch 库 为整个操作系统提供了优化的调度 并为每个平台进行了广泛的调整 在更受限制的环境中 多线程调度程序开销可能无法被接受 在这里 Swift 的并发模型 是通过单线程合作队列实现的 同样的 Swift 代码 在两种环境下都能工作 因为抽象模型足够灵活 可以映射到不同的运行时环境
此外 与基于回调的库的互操作性 从一开始就内置于 Swift 的 async/await 支持中 withCheckedContinuation 操作 允许暂停 task 并在稍后响应回调时恢复它 这让我们能够与现有的 管理任务的库整合
Swift 并发运行时中的 actor 标准实现 是在 actor 上执行的无锁的 task 队列 但这不是唯一可能的实现 在一个更受限制的 环境中 可能没有原子操作 这样就可以使用另一种 并发原语 如自旋锁 如果该环境是单线程的 就不需要同步 但不管怎样 actor 模型都会为程序维护 抽象的并发模型 你还可以把同样的代码带到 多线程的环境中 在 Swift 5.9 中 自定义 actor 执行器 允许特定 actor 实现自己的同步机制 这使得 actor 更加灵活 可以适应现有环境 我们来举个例子 这里我们考虑一个 管理数据库连接的 actor Swift 确保了对 这个 actor 的存储的互斥访问 所以不会有任何对数据库的并发访问 不过 如果你需要对同步的具体方式 进行更多的控制呢? 例如 如果你想为你的数据库连接 使用一个特定的调度队列 也许是因为这个队列是与 其他没有采用 actor 的代码 共享的 那怎么办? 有了自定义 actor 执行器 你就可以做到了
在这里 我们给 actor 添加了一个串行调度队列 并实现了 unownedExecutor 属性 产生了与该调度队列相对应的执行器 有了这个变化 我们的 actor 实例的所有同步 都将通过该队列发生
当你从 actor 外部对 pruneOldEntries 的调用进行 “await” 时 它将在相应的队列上 执行一个 dispatch-async 这让你对单个 actor 如何提供同步 有了更多的控制 甚至可以让你将一个 actor 与其他还没有使用 actor 的代码同步 有可能是因为它是用 Objective-C 或 C++ 写的
通过调度队列实现 actor 的同步 是因为调度队列符合 新的 SerialExecutor 协议 你可以通过定义一个符合该协议的 新类型来提供你自己的同步机制 以便与 actor 一起使用 该协议只有少数核心操作: 检查代码是否已经 在执行器的上下文中执行 例如 我们是否在主线程上运行?
提取对执行器的无主引用 以允许对其进行访问 而不需要过多的引用计数流量 还有最核心的操作:enqueue 它需要取得 “job” 执行器的所有权 job 是异步任务的一部分 需要在执行器上同步运行 enqueue 被调用时 执行器有责任 在没有其他代码 在串行执行器上运行的时候运行该 job 例如 调度队列的 enqueue 会 在该队列上调用派发 async
Swift Concurrency 已经使用了几年 其由 task 和 actor 组成的抽象模型 涵盖了大量的并发编程任务 抽象模型本身相当灵活 使其能够适应不同的执行环境 从 iPhone 到 Apple Watch 再到服务器等 它还允许在关键点上 进行定制 使其能够 与尚未完全采用 Swift Concurrency 的代码进行互操作 了解更多信息 请查看我们的“幕后”演讲 以及“超越基础的结构化并发” 我想用以下的案例研究 来总结一下 Swift 的运行环境 与我们习惯看到的 iOS 或 MacOS App 非常不同 FoundationDB 是一个分布式数据库 它为在商用硬件上运行的 非常大的键值存储 提供可扩展的解决方案 并支持各种平台 包括 macOS、Linux 和 Windows FoundationDB 是一个开源项目 有一个用 C++ 编写的大型代码库 该代码是高度异步的 有自己的分布式 actor 和运行时 为测试目的提供至关重要的 确定性模拟环境 FoundationDB 希望对 他们的代码库进行现代化改造 并发现 Swift 在性能、 安全性和代码简洁性方面 是个不错的选择 完全重写是巨大而冒险的工作 相反 我们利用了 Swift 的互操作性 来整合到现有的代码库中 例如 这里是 FoundationDB 的 “主数据” actor 的 C++ 实现的一部分
内容很多 但你不需要理解所有这些 C++ 不过 我想指出该代码的几个关键方面 首先 C++ 没有 async/await 所以 FoundationDB 有 他们自己的类似预处理器的方法 来模拟它
像许多 C++ 代码库一样 他们实现了自己的 C++ Future 类型 来管理异步任务 它们与显式消息传递配对 以发送对请求的响应 注意发送回复 与从函数中返回的仔细配对 最后 FoundationDB 有自己的引用计数的智能指针 来帮助自动管理内存 我们可以在 Swift 中 更简洁地实现这些
这就好多了 这个函数可以直接作为 Swift 的一个同步函数来实现 我们有一个正常的返回类型 和正常的返回语句 来提供对这个请求的响应 所以你会一直保持同步 我们有 “await” 来表示暂停点 其方式与所有其他 Swift 异步代码相同 这段 Swift 代码与 C++ 的 Future 类型相适应 使用 continuation 进行改编
我们在这里使用了一些 C++ 的类型 C++ 中的 MasterData 类型 使用的是引用计数的智能指针 通过在 C++ 中标注该类型 Swift 编译器 可以像使用其他类一样使用该类型 自动为我们管理引用计数
其他类型 如请求和回复类型 是在 Swift 中直接使用的 C++ 值类型 而且 互操作性是双向的 这个异步函数 以及由 Swift concurrency 模型 引入的所有工作 都可以在 FoundationDB 现有的确定性运行时上运行 因此 我们可以获得 我们想要的 Swift 好处 与现有的 C++ 对接 以实现逐步采用
在这部分中 我们已经讨论了很多内容 我们描述了参数聚合 和宏等功能 这些功能使 API 更具表现力 可以帮助你更快地编写更好的代码 我们谈到了 Swift 在 性能敏感代码中的使用 以及引入不可复制类型 来提供资源管理 而不需要引用计数的开销 然后我们深入探讨了 C++ 的互操作性 它为在 Swift 中使用 C++ API 提供了支持 反之亦然 让 Swift 的好处 更容易地惠及你的更多代码
最后 我们谈到了 Swift 灵活的并发模型 如何适应不同设备和语言的 种种环境 使并发变得更容易和安全 参数聚合、宏、不可复制类型 以及 Swift 5.9 中的 所有其他语言增强功能 都是通过 Swift Evolution 过程 公开设计和开发的 而社区反馈对塑造这些功能至关重要 Swift 5.9 是整个 Swift 社区成员 无数次贡献的结晶 他们开展了积极的设计讨论、 错误报告、拉动请求、教育内容等等 感谢你让 Swift 5.9 成为一个伟大的版本 ♪ ♪
-
-
3:06 - Hard-to-read compound ternary expression
let bullet = isRoot && (count == 0 || !willExpand) ? "" : count == 0 ? "- " : maxDepth <= 0 ? "▹ " : "▿ "
-
3:19 - Familiar and readable chain of if statements
let bullet = if isRoot && (count == 0 || !willExpand) { "" } else if count == 0 { "- " } else if maxDepth <= 0 { "▹ " } else { "▿ " }
-
3:30 - Initializing a global variable or stored property
let attributedName = AttributedString(markdown: displayName)
-
3:46 - In 5.9, if statements can be an expression
let attributedName = if let displayName, !displayName.isEmpty { AttributedString(markdown: displayName) } else { "Untitled" }
-
4:31 - In Swift 5.7, errors may appear in a different place
struct ContentView: View { enum Destination { case one, two } var body: some View { List { NavigationLink(value: .one) { // The issue actually occurs here Text("one") } NavigationLink(value: .two) { Text("two") } }.navigationDestination(for: Destination.self) { $0.view // Error occurs here in 5.7 } } }
-
4:47 - In Swift 5.9, you now receive a more accurate compiler diagnostic
struct ContentView: View { enum Destination { case one, two } var body: some View { List { NavigationLink(value: .one) { //In 5.9, Errors provide a more accurate diagnostic Text("one") } NavigationLink(value: .two) { Text("two") } }.navigationDestination(for: Destination.self) { $0.view // Error occurs here in 5.7 } } }
-
5:47 - An API that takes a request type and evaluates it to produce a strongly typed value
struct Request<Result> { ... } struct RequestEvaluator { func evaluate<Result>(_ request: Request<Result>) -> Result } func evaluate(_ request: Request<Bool>) -> Bool { return RequestEvaluator().evaluate(request) }
-
6:03 - APIs that abstract over concrete types and varying number of arguments
let value = RequestEvaluator().evaluate(request) let (x, y) = RequestEvaluator().evaluate(r1, r2) let (x, y, z) = RequestEvaluator().evaluate(r1, r2, r3)
-
6:35 - Writing multiple overloads for the evaluate function
func evaluate<Result>(_:) -> (Result) func evaluate<R1, R2>(_:_:) -> (R1, R2) func evaluate<R1, R2, R3>(_:_:_:) -> (R1, R2, R3) func evaluate<R1, R2, R3, R4>(_:_:_:_:)-> (R1, R2, R3, R4) func evaluate<R1, R2, R3, R4, R5>(_:_:_:_:_:) -> (R1, R2, R3, R4, R5) func evaluate<R1, R2, R3, R4, R5, R6>(_:_:_:_:_:_:) -> (R1, R2, R3, R4, R5, R6)
-
6:47 - Overloads create an arbitrary upper bound for the number of arguments
//This will cause a compiler error "Extra argument in call" let results = evaluator.evaluate(r1, r2, r3, r4, r5, r6, r7)
-
7:12 - Individual type parameter
<each Result>
-
7:36 - Collapsing the same set of overloads into one single evaluate function
func evaluate<each Result>(_: repeat Request<each Result>) -> (repeat each Result)
-
8:21 - Calling updated evaluate function looks identical to calling an overload
struct Request<Result> { ... } struct RequestEvaluator { func evaluate<each Result>(_: repeat Request<each Result>) -> (repeat each Result) } let results = RequestEvaluator.evaluate(r1, r2, r3)
-
10:01 - It isn't clear why an assert function fails
assert(max(a, b) == c)
-
10:20 - XCTest provides an assert-equal operation
XCAssertEqual(max(a, b), c) //XCTAssertEqual failed: ("10") is not equal to ("17")
-
11:02 - Assert as a macro
#assert(max(a, b) == c)
-
11:42 - Macros are distributed as packages
import PowerAssert #assert(max(a, b) == c)
-
12:07 - Macro declaration for assert
public macro assert(_ condition: Bool)
-
12:26 - Uses are type checked against the parameters
import PowerAssert #assert(max(a, b)) //Type 'Int' cannot be a used as a boolean; test for '!= 0' instead
-
12:52 - A macro definition
public macro assert(_ condition: Bool) = #externalMacro( module: “PowerAssertPlugin”, type: “PowerAssertMacro" )
-
13:11 - Swift compiler passes the source code for the use of the macro
#assert(a == b)
-
13:14 - Compiler plugin produces new source code, which is integrated back into the Swift program
PowerAssert.Assertion( "#assert(a == b)" ) { $0.capture(a, column: 8) == $0.capture(b, column: 13) }
-
13:33 - Macro declarations include roles
// Freestanding macro roles @freestanding(expression) public macro assert(_ condition: Bool) = #externalMacro( module: “PowerAssertPlugin”, type: “PowerAssertMacro" )
-
13:53 - New Foundation Predicate APIs uses a `@freestanding(expression)` macro role
let pred = #Predicate<Person> { $0.favoriteColor == .blue } let blueLovers = people.filter(pred)
-
14:14 - Predicate expression macro
// Predicate expression macro @freestanding(expression) public macro Predicate<each Input>( _ body: (repeat each Input) -> Bool ) -> Predicate<repeat each Input>
-
14:48 - Example of a commonly used enum
enum Path { case relative(String) case absolute(String) }
-
15:01 - Checking a specific case, like when filtering all absolute paths
let absPaths = paths.filter { $0.isAbsolute }
-
15:09 - Write an `isAbsolute` check as a computer property...
extension Path { var isAbsolute: Bool { if case .absolute = self { true } else { false } } }
-
15:12 - ...And another for `isRelative`
extension Path { var isRelative: Bool { if case .relative = self { true } else { false } } }
-
15:17 - Augmenting the enum with an attached macro
@CaseDetection enum Path { case relative(String) case absolute(String) } let absPaths = paths.filter { $0.isAbsolute }
-
15:36 - Macro-expanded code is normal Swift code
enum Path { case relative(String) case absolute(String) //Expanded @CaseDetection macro integrated into the program. var isAbsolute: Bool { if case .absolute = self { true } else { false } } var isRelative: Bool { if case .relative = self { true } else { false } } }
-
16:57 - Observation in SwiftUI prior to 5.9
// Observation in SwiftUI final class Person: ObservableObject { @Published var name: String @Published var age: Int @Published var isFavorite: Bool } struct ContentView: View { @ObservedObject var person: Person var body: some View { Text("Hello, \(person.name)") } }
-
17:25 - Observation now
// Observation in SwiftUI @Observable final class Person { var name: String var age: Int var isFavorite: Bool } struct ContentView: View { var person: Person var body: some View { Text("Hello, \(person.name)") } }
-
17:42 - Observable macro works with 3 macro roles
@attached(member, names: ...) @attached(memberAttribute) @attached(conformance) public macro Observable() = #externalMacro(...).
-
17:52 - Unexpanded macro
@Observable final class Person { var name: String var age: Int var isFavorite: Bool }
-
18:05 - Expanded member attribute role
@Observable final class Person { var name: String var age: Int var isFavorite: Bool internal let _$observationRegistrar = ObservationRegistrar<Person>() internal func access<Member>( keyPath: KeyPath<Person, Member> ) { _$observationRegistrar.access(self, keyPath: keyPath) } internal func withMutation<Member, T>( keyPath: KeyPath<Person, Member>, _ mutation: () throws -> T ) rethrows -> T { try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation) } }
-
18:12 - Member attribute role adds `@ObservationTracked` to stored properties
@Observable final class Person { @ObservationTracked var name: String @ObservationTracked var age: Int @ObservationTracked var isFavorite: Bool internal let _$observationRegistrar = ObservationRegistrar<Person>() internal func access<Member>( keyPath: KeyPath<Person, Member> ) { _$observationRegistrar.access(self, keyPath: keyPath) } internal func withMutation<Member, T>( keyPath: KeyPath<Person, Member>, _ mutation: () throws -> T ) rethrows -> T { try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation) } }
-
18:16 - The @ObservationTracked macro adds getters and setters to stored properties
@Observable final class Person { @ObservationTracked var name: String { get { … } set { … } } @ObservationTracked var age: Int { get { … } set { … } } @ObservationTracked var isFavorite: Bool { get { … } set { … } } internal let _$observationRegistrar = ObservationRegistrar<Person>() internal func access<Member>( keyPath: KeyPath<Person, Member> ) { _$observationRegistrar.access(self, keyPath: keyPath) } internal func withMutation<Member, T>( keyPath: KeyPath<Person, Member>, _ mutation: () throws -> T ) rethrows -> T { try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation) } }
-
18:33 - All that Swift code is folded away in the @Observable macro
@Observable final class Person { var name: String var age: Int var isFavorite: Bool }
-
23:59 - A wrapper for a file descriptor
struct FileDescriptor { private var fd: CInt init(descriptor: CInt) { self.fd = descriptor } func write(buffer: [UInt8]) throws { let written = buffer.withUnsafeBufferPointer { Darwin.write(fd, $0.baseAddress, $0.count) } // ... } func close() { Darwin.close(fd) } }
-
24:30 - The same FileDescriptor wrapper as a class
class FileDescriptor { private var fd: CInt init(descriptor: CInt) { self.fd = descriptor } func write(buffer: [UInt8]) throws { let written = buffer.withUnsafeBufferPointer { Darwin.write(fd, $0.baseAddress, $0.count) } // ... } func close() { Darwin.close(fd) } deinit { self.close(fd) } }
-
25:05 - Going back to the struct
struct FileDescriptor { private var fd: CInt init(descriptor: CInt) { self.fd = descriptor } func write(buffer: [UInt8]) throws { let written = buffer.withUnsafeBufferPointer { Darwin.write(fd, $0.baseAddress, $0.count) } // ... } func close() { Darwin.close(fd) } }
-
26:06 - Using Copyable in the FileDescriptor struct
struct FileDescriptor: ~Copyable { private var fd: CInt init(descriptor: CInt) { self.fd = descriptor } func write(buffer: [UInt8]) throws { let written = buffer.withUnsafeBufferPointer { Darwin.write(fd, $0.baseAddress, $0.count) } // ... } func close() { Darwin.close(fd) } deinit { Darwin.close(fd) } }
-
26:35 - `close()` can also be marked as consuming
struct FileDescriptor { private var fd: CInt init(descriptor: CInt) { self.fd = descriptor } func write(buffer: [UInt8]) throws { let written = buffer.withUnsafeBufferPointer { Darwin.write(fd, $0.baseAddress, $0.count) } // ... } consuming func close() { Darwin.close(fd) } deinit { Darwin.close(fd) } }
-
26:53 - When `close()` is called, it must be the final use
let file = FileDescriptor(fd: descriptor) file.write(buffer: data) file.close()
-
27:20 - Compiler errors instead of runtime failures
let file = FileDescriptor(fd: descriptor) file.close() // Compiler will indicate where the consuming use is file.write(buffer: data) // Compiler error: 'file' used after consuming
-
28:52 - Using C++ from Swift
// Person.h struct Person { Person(const Person &); Person(Person &&); Person &operator=(const Person &); Person &operator=(Person &&); ~Person(); std::string name; unsigned getAge() const; }; std::vector<Person> everyone(); // Client.swift func greetAdults() { for person in everyone().filter { $0.getAge() >= 18 } { print("Hello, \(person.name)!") } }
-
29:51 - Using Swift from C++
// Geometry.swift struct LabeledPoint { var x = 0.0, y = 0.0 var label: String = “origin” mutating func moveBy(x deltaX: Double, y deltaY: Double) { … } var magnitude: Double { … } } // C++ client #include <Geometry-Swift.h> void test() { Point origin = Point() Point unit = Point::init(1.0, 1.0, “unit”) unit.moveBy(2, -2) std::cout << unit.label << “ moved to “ << unit.magnitude() << std::endl; }
-
35:30 - An actor that manages a database connection
// Custom actor executors actor MyConnection { private var database: UnsafeMutablePointer<sqlite3> init(filename: String) throws { … } func pruneOldEntries() { … } func fetchEntry<Entry>(named: String, type: Entry.Type) -> Entry? { … } } await connection.pruneOldEntries()
-
35:58 - MyConnection with a serial dispatch queue and a custom executor
actor MyConnection { private var database: UnsafeMutablePointer<sqlite3> private let queue: DispatchSerialQueue nonisolated var unownedExecutor: UnownedSerialExecutor { queue.asUnownedSerialExecutor() } init(filename: String, queue: DispatchSerialQueue) throws { … } func pruneOldEntries() { … } func fetchEntry<Entry>(named: String, type: Entry.Type) -> Entry? { … } } await connection.pruneOldEntries()
-
36:44 - Dispatch queues conform to SerialExecutor protocol
// Executor protocols protocol Executor: AnyObject, Sendable { func enqueue(_ job: consuming ExecutorJob) } protocol SerialExecutor: Executor { func asUnownedSerialExecutor() -> UnownedSerialExecutor func isSameExclusiveExecutionContext(other executor: Self) -> Bool } extension DispatchSerialQueue: SerialExecutor { … }
-
39:22 - C++ implementation of FoundationDB's "master data" actor
// C++ implementation of FoundationDB’s “master data” actor ACTOR Future<Void> getVersion(Reference<MasterData> self, GetCommitVersionRequest req) { state std::map<UID, CommitProxyVersionReplies>::iterator proxyItr = self->lastCommitProxyVersionReplies.find(req.requestingProxy); ++self->getCommitVersionRequests; if (proxyItr == self->lastCommitProxyVersionReplies.end()) { req.reply.send(Never()); return Void(); } wait(proxyItr->second.latestRequestNum.whenAtLeast(req.requestNum - 1)); auto itr = proxyItr->second.replies.find(req.requestNum); if (itr != proxyItr->second.replies.end()) { req.reply.send(itr->second); return Void(); } // ... }
-
40:18 - Swift implementation of FoundationDB's "master data" actor
// Swift implementation of FoundationDB’s “master data” actor func getVersion( myself: MasterData, req: GetCommitVersionRequest ) async -> GetCommitVersionReply? { myself.getCommitVersionRequests += 1 guard let lastVersionReplies = lastCommitProxyVersionReplies[req.requestingProxy] else { return nil } // ... var latestRequestNum = try await lastVersionReplies.latestRequestNum .atLeast(VersionMetricHandle.ValueType(req.requestNum - UInt64(1))) if let lastReply = lastVersionReplies.replies[req.requestNum] { return lastReply } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。