大多数浏览器和
Developer App 均支持流媒体播放。
-
活用 Swift 类型推论
在不影响类型安全的前提下,Swift 可借助类型推理帮助你编写简洁明了的代码。我们将带你了解编译器如何找出代码中线索来解决类型推理难题。并了解 Xcode 12 如何集成错误跟踪,以帮助你在编程时捕捉并纠正错误。
资源
相关视频
WWDC20
-
下载
(你好 WWDC 2020)
你好 欢迎来到 WWDC (活用 Swift 类型推论) 大家好 我叫 Holly 是 Swift 编译器团队的一位工程师 欢迎观看《活用 Swift 类型推论》讲座 Swift 广泛使用类型推论 在不牺牲代码安全性的同时 实现了简洁的语法 今天 我们会讨论 你在何时可以利用类型推论 我们会了解类型推论如何在编译器中运行 最后 我们会看看 如何通过 Swift 和 Xcode 来了解并修复你代码中的编译器错误 开始之前 我们先回顾一下类型推论是什么 当编译器能够根据上下文情境 编写出显式类型注释和其它赘余详情时 类型推论使你可以在你的源代码中 省略这些细节信息 这个情况中 源代码中没有 x 的类型 但编译器根据赋值中提供的值 也就是一个字符串字面量 推断 x 的类型为字符串
x 的类型 原本可以通过在 x 后面使用冒号 或者使用“as”关键字 迫使等式右边变成字符串 来用一个类型注释进行显式指定 这只是一个很小的例子 但是当代码非常依赖于类型推论时 比如说在一个 SwiftUI 项目里 事情就变得有意思多了 许多 SwiftUI app 通过组合小的、可重复使用的视图 来创建庞大且复杂的界面 那么这类代码 在那些可重复使用的组件的调用点上 是非常依赖于类型推论的 我们现在要深入研究 SwiftUI app 中 在这些调用点利用类型推论的代码 接着我们会了解编译器 是如何完成类型推论这样一个“拼图”的 最后 我们会看看 如何通过 Swift 编译器和 Xcode 来了解并修复代码中的编译器错误
我们会用我正在与团队 一同开发的一个 app 作为示例 它叫 Fruta 可以用它来购买奶昔 目前 这个 app 会显示一个奶昔列表 你可以直接在预览中看到
我想添加一项功能 使用户能够搜索奶昔 我们先来看看当前奶昔列表的执行情况
SmoothieList 主体从一个奶昔数组中 创建一个 SwiftUI 列表 将每款奶昔 映射到一个 SmoothieRowView 中 要想添加搜索功能 我需要根据 用户搜索的一个字符串来过滤出奶昔 所以 SmoothieList 需要一个状态属性来储存这个字符串
我构建了一个 可重复使用的新视图 叫做 FilteredList 和 List 十分相似 但它能让我向初始化器传递额外的两个 指定如何过滤奶昔的引数
第一个是 KeyPath 用于我想要作为过滤条件的奶昔属性 第二个是一个函数 用于计算根据这个属性 奶昔是否应该被包括在列表中
在我们的情况中 会通过奶昔的名称来过滤它们 并且当名称中含有 searchPhrase 作为子字符串时 这款奶昔就会被包括在内 hasSubstring 方法 会在名称中搜查 searchPhrase 而且当 searchPhrase 是一个空字符串时 它仍然会返回真
从调用点来看不是很明显 但这个对 FilteredList 初始化器的调用 非常依赖于类型推论 我们来谈谈 这个代码依赖于类型推论的缘由 以及调用点如何利用 类型推论来得到更简明的代码 为了了解从这个调用点省略的附带详情 我们需要查看 FilteredList 的声明以及它的初始化器
由于 FilteredList 是一个通用视图 它应该可以用于 客户需要在他们列表上显示的 任何类型的数据和内容 这种灵活性是通过使用泛型来实现的
我介绍了三种类型的参数 都是占位符 会在调用点被实际类型所替换 这些实际类型 正式名称叫做“具体类型” 一般在调用点处被指定 否则会由编译器推断得出 在这个情况下 Element 是数据数组中 元素类型的占位符 FilterKey 是 作为数据过滤条件的 Element 上 特定属性类型的占位符 以及 RowContent 是显示在 列表每一行的视图类型的占位符 我现在有了这三种类型的参数 就可以 在初始化器的参数列表中使用它们 初始化器参数列表中 类型参数出现的每个位置 都有机会在调用点利用类型推论 前提是 要为编译器提供一个引数 来提示它应该用什么来替换类型参数 我们稍后会看一下 这是如何执行的 但目前 我们正在构建列表 所以会把重点放在每个参数的类型上 首先 FilteredList 会使用待映射的数据 也就是一个 Element 数组 来进行初始化
下一个参数会作为过滤条件 它是 Element 的一个特定属性 也就是一个带有 Element 基本类型 和一个 FilterKey 值类型的 KeyPath
接下来的参数是 isIncluded 闭包 它是一个将 FilterKey 作为输入值 并返回 Bool 的函数类型 闭包被标记为“逃逸” 因为 FilteredList 会需要 把这个闭包存储在一项属性中
最后一个参数 是用来将数据元素映射到视图的一个闭包 也就是将 Element 作出输入值 并会返回 RowContent 的函数类型
这个闭包也被标记为“逃逸” 因为它需要被储存
要注意 这个参数被标记为 ViewBuilder 以便在调用点启用 闭包引数中的 SwiftUI DSL 语法 SwiftUI DSL 使你能够声明多个子视图 需要通过在闭包主体中将它们列出 接着 ViewBuilder 会把子视图 收集到一个元组中 从而让父视图可以使用 这是 FilteredList 的完整初始化器 当一个 FilteredList 被初始化时 它的类型参数会被“具体类型”取代 我们来并排查看一下这个初始化器 与 SmoothieList 中的调用点 来弄明白调用点是如何依赖于类型推论的
可以发现调用点非常整洁 调用点上没有编写显式类型注释 但它仍然为编译器 提供了它所需的所有信息 来为 FilteredList 的每个类型参数 推断出具体类型
我们来看看 如果所有引数类型 都在代码中被显式指定 那会是什么样子 我们将使用类型参数的占位符 来查看 类型推论需要填充具体类型的确切位置
首先 FilteredList 的三个类型参数 本可以在调用点的 FilteredList 后面的 角括号中被显式指定
第一个参数的类型 即一个 Element 数组 本可以在“smoothies”引数之后 通过使用“as”关键字被指定
在第二个参数中 Element 是 KeyPath 基本类型 这原本可以通过 在反斜杠和点之间的 KeyPath 字面量中 编写一个显式基本类型来指定
整个 KeyPath 类型原本可以通过 在 KeyPath 字面量之后 使用“as”关键字来指定
接着 isIncluded 闭包类型原本可以通过 在闭包主题中使用一个类型注释来指定
最后 RowContent 闭包类型原本也可以通过 在闭包主体中使用一个类型注释来被指定
现在 我一开始写的源代码 充斥着许多赘余的类型注释 其中还包括一堆占位符 用于需要被类型推论填充的类型
与其人工推算出 这些占位符都应该由什么来填充 取而代之的 我依赖于类型推论 来弄清这些类型究竟应该是什么 换句话说 类型推论帮助你更快地编写源代码 因为你不需要知道 如何在你的代码中 确切地拼写出所有这些类型
我们来说说编译器是如何弄清 这些占位符的替代内容的 你可以把类型推论想象成一个拼图 类型推论算法通过使用源代码中的线索 来填充缺失的部分 从而完成拼图
在填充拼图块的过程中 还能不断发现关于剩余拼图块的更多线索
现在来看看 我们能否像编译器运用类型推断 为我们完成的一样 通过使用源代码的线索 来一起完成这个拼图 首先 “smoothies”引数 会告诉我们为 Element 填充的内容 而我们知道 “smoothies”是一个已经具有类型的属性 通过使用快速帮助 我们可以看到 属性是一个“Smoothie”元素数组 所以 Element 可以用“Smoothie”来填充
现在我们填好了 Element 的其中一个占位符 我们就可以用同样的“Smoothie”类型 来替换其它所有 Element
现在 通过填充其它 Element 占位符 又解开了一条 FilterKey 的具体类型的线索 因为现在我们知道 KeyPath 字面量 指的是 Smoothie.title
通过再次使用快速帮助 我们看到 Smoothie.title 是一个字符串
所以 FilterKey 可以用“字符串”来填充 我们现在知道了 FilterKey 是什么 就可以把其它的 FilterKey 占位符 都替换为“字符串”
拼图的最后一片是 RowContent 也就是 尾随 ViewBuilder 闭包的返回类型
由于这个闭包在主题中只有一个视图 ViewBuilder 将返回单个子视图 其中包含 SmoothieRowView 类型 同样的 现在我们弄清了 RowContent 是什么 就可以将最后一个占位符 替换为 SmoothieRowView 了 就这样 我们放入了最后一片拼图 这就是编译器 用来从你的代码中推断类型的技巧 你编写的代码为编译器提供了可用线索 这个算法的每一步都会揭开更多信息 并为后续步骤所使用 但是 源代码中的某一条线索也可能 会让编译器使用 与其它拼图不匹配的具体类型 来填充占位符 如果其中一块拼图不匹配 导致无法完成整个拼图的话 就意味着源代码中有一个错误 我们来讨论一下 编译器是如何修改它的类型推论策略 来完成存在源代码错误的拼图的 之后 我们会看看 你可以如何利用你的工具 来了解并修复这些错误 我们回到编译器 为 FilterKey 推断具体类型这一步 还记得在之前一步中 编译器推断出 Smoothie 是 KeyPath 基本类型 接着 通过查询 Smoothie.title 类型 它又用这条信息 得出了 FilterKey 的具体类型
如果我犯了一个错 在 KeyPath 字面量中使用了错误属性 编译器就会试图将 FilterKey 类型 推断为这个错误属性的类型 在这个情况中 也就是 Bool
接着 它会用 同样的错误类型来继续填充 其它的 FilterKey 占位符
现在 如果我们查看一下 这个 Bool 类型在 isIncluded 闭包中 是如何被使用的 就会很明显地发现这部分不匹配 因为 Bool 没有任何名为 hasSubstring 的方法 所以 编译器需要汇报出错信息 (错误) 我们在不可避免地会在编写代码时出错 所以在设计编程语言以及它们的工具时 必须把我们作为人类的局限性考虑进去 Swift 编译器的设计目的 就在于捕获这些错误 它通过将错误跟踪集成到类型推论算法 来为今后的错误消息所使用 (集成错误跟踪) 在类型推论中 编译器将记录它遇到的任何错误信息 接着 编译器使用启发式 来修复错误 以继续进行类型推论
类型推论一结束 编译器就会汇报它收集到的所有错误 通常会包含可操作的修复方案 来自动修复源代码中的错误 或者包含有关编译器推断出的 可能会导致错误的具体类型 在 Xcode 11.4 和 Swift 5.2 中 我们为很多错误消息 引入了集成错误跟踪 而在 Xcode 12 和 Swift 5.3 中 编译器将这个新技巧 应用在了表达式中的所有错误消息上 我知道 大家在尝试修复 自己代码中的编译器错误时 都会感到非常懊恼沮丧 但错误消息经过特别设计 就像是内置在编译器中的 一个结对编程器 能够捕获错误 并帮助你修复它们 而不会让你的错误成为代码中的漏网之鱼
我们跳转到 Xcode 中 来看看如何使用这些工具 来在编写 Swift 代码的过程中 修复这些错误 在编写任何代码之前 我们会先打开 Xcode 菜单 在“行为”中 点击“编辑行为”
我要添加一个行为 在构建失败时 自动向我显示问题导航器 现在 每当我的项目构建失败后 我将会看到项目中所有的错误 我们在这里看到 当前对于 SmoothieList 的执行 和它的预览 我已经在我的项目中 添加了 FilteredList 你可以在项目导航中看到 但在我们把列表 替换为 FilteredList 之前 我需要在列表顶端添加一个搜索框 我已经添加了一个状态属性 用于储存用户的搜索短语 那么现在 我要在 VStack 中 向列表顶端添加一个 TextField
我把 TextField 命名为“搜索” 然后把搜索短语传过来 现在我要使用 CommandB 来进行构建 以确保我不会犯任何错误
请注意 在我刚添加的这行代码中 出现了一个编译器错误
我们通过点击它来展开错误
似乎是因为我使用的一个引数的属性 是“字符串” 而它无法与 文本框初始化器预期的参数类型 也就是“绑定” 相兼容 我错误地把 搜索框的值传递了 而不是它的绑定 而 Swift 编译器能够获知 绑定并没有一个可兼容的类型 于是会使用美元符号 向我提供一个 指向绑定的修复方案
接着 我想用 新的 FilteredList 替换这个列表
而我打算再构建一次 以确保我不会犯任何错误
但现在看来又出现了另一个错误 如果我展开这个错误 就能看到这是因为 Smoothie 不符合 FilterList 的可识别要求 这也许挺让人困惑的 因为这部分代码中找不到“可识别”
注意 在左侧的问题导航器中 这个错误附有一条编译器注释
使用新的集成错误跟踪 编译器会记录在类别推论期间 遇到这个问题时的状况 来使编译器留下痕迹 从而直接引导你通过编译器注释 去查看你代码的其它部分 这些注释有助于你 将源代码编辑器中看到的错误 与你项目中 其它文件的关键信息联系在一起 而且你会在 Swift 5.3 和 Xcode 12 中看到更多的注释 我想将这些注释与错误并排起来进行查看 所以我要关闭 canvas 可以使用 Command-Enter 快捷键来完成 现在 我要同时按住 Option 和 Shift 键 并点击注释来显示目的地选择器
现在我可以用方向键把它挪到右边 然后按回车键 来在新的编辑器中打开文件
现在 我们看到的是 FilteredList 声明 如果我将注释展开 它会指出 编译器推断出 Element 的具体类型 是 Smoothie
在这个声明中 我能看到 Element 或 Smoothie 必须符合“可识别”
我一定是忘了给 Smoothie 添加这个符合性 就试图把它用在 FilterList 中了 在左侧的编辑器中 我按住 Command 键并点击 Smoothie 然后使用跳转到定义 我可以在这里 将要求的复合型添加到“可识别” Smoothie 已经有了一项叫做“id”的属性了 所以可识别协议的要求已经执行了 但是为了反复检查 我要再构建一次 以确保错误确实被修复了
看来现在我们已经没有错误了 那么 我们再来看看右侧的编辑器 并点击“返回”按钮 回到 SmoothieList 现在 如果我对 FilteredList 初始化器的 其中一个引数使用快速帮助 我就可以看到 由编译器推断出的具体类型了 我们试着对 KeyPath 字面量使用 快速帮助 只需按住 Option 键并点击
就像我们之前看到的 完成类型推论拼图一样 编译器根据推断出的 KeyPath 基本类型 推断出标题类型为字符串 而且要记得 编译器根据上一行中 Smoothie 的引数 推断出了基本类型为 Smoothie
在结束前 我想确保 可以使用 canvas 来进行过滤操作 我可以使用快捷键绑定 Command-Option-Enter 重新打开 canvas
然后我要在预览提供器中 添加另一个预览 让它在搜索栏中显示一些文本
现在 预览提供器正在对 我想测试的所有搜索短语进行迭代遍历 在这个情况中 包括是空字符串和字符串“Berry” 接着 如果我用 Command-Option-P 来刷新预览 我可以看到它成功运行了 我能看到 第二个预览中的搜索框中键入了“Berry” 而且列表中 只有名称里包含“Berry”的奶昔
当我在代码中输入错别字或者其它错误时 编译器错误消息 会提供有用信息 且具有可操作性 此外 编译器中新的集成错误跟踪 使编译器能够收集更多失败相关信息 并通过注释显示出来 这些只是你在使用 Swift 5.3 和 Xcode 12 的过程中 会发现一小部分改进 在这个讲座中 我们学到了 当界面由可重复使用的视图组成时 SwiftUI 代码非常依赖于类型推论 接着 我们了解了类型推论 是如何通过源代码中的线索 得出并填充被省略的附带详情的 最后 我们学习了编译器如何将错误跟踪 集成到类型推论算法中 以记录更多信息 并为错误消息留下痕迹 从而帮助你了解并修复错误 如果想要做进一步了解 你可以阅读 Swift 发布的博客 内容是关于编译器的新集成错误跟踪 叫做新的诊断架构 要想进一步了解构建 SwiftUI 视图 我推荐大家关注 developer.apple.com 的 SwiftUI 教程 最后 请去查看 WWDC 2018 中的 Swift 泛型 来更加深入地了解 Swift 中的泛型
感谢大家的观看
-
-
2:56 - SmoothieList
import SwiftUI struct SmoothieList: View { var smoothies: [Smoothie] @State var searchPhrase = "" var body: some View { FilteredList( smoothies, filterBy: \.title, isIncluded: { title in title.hasSubstring(searchPhrase) } ) { smoothie in SmoothieRowView(smoothie: smoothie) } } } extension String { /// Returns `true` if this string contains the provided substring, /// or if the substring is empty. Otherwise, returns `false`. /// /// - Parameter substring: The substring to search for within /// this string. func hasSubstring(_ substring: String) -> Bool { substring.isEmpty || contains(substring) } }
-
3:53 - FilteredList
import SwiftUI public struct FilteredList<Element, FilterKey, RowContent>: View where Element: Identifiable, RowContent: View { private let data: [Element] private let filterKey: KeyPath<Element, FilterKey> private let isIncluded: (FilterKey) -> Bool private let rowContent: (Element) -> RowContent public init( _ data: [Element], filterBy key: KeyPath<Element, FilterKey>, isIncluded: @escaping (FilterKey) -> Bool, @ViewBuilder rowContent: @escaping (Element) -> RowContent ) { self.data = data self.filterKey = key self.isIncluded = isIncluded self.rowContent = rowContent } public var body: some View { let filteredData = data.filter { isIncluded($0[keyPath: filterKey]) } return List(filteredData, rowContent: rowContent) } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。