概览
小伙伴们可能不知道:在 Swift 语言中隐藏着大量看似“其貌不扬”实则却让秃头码农们“高世骇俗”,堪称卧虎藏龙的各种秘技。
其中,有一枚“不起眼”的小家伙称之为键路径(Key Paths)。如若将其善加利用,必将在实际撸码中大放异彩,如虎添翼!
在本篇博文中,您将学到如下内容:
本篇和下一篇皆为看似“平淡无奇”的键路径“凤凰涅槃”、逆袭重生的“励志”博文!到底如何?且看分解!
闲言少叙,Let’s change our destiny against the heavens!!!😉
本文对应的视频课在此,请小伙伴们恣意观赏:
Swift中强大的KeyPaths(键路径)机制趣谈(上)
1. 一窥门径:键路径(Key Paths)初步
我们知道 Swift 语言最初的设计重点是编译时安全和静态类型。因此,它势必会缺乏那些更加关注运行时语言(如 Objective-C、Ruby 和 JavaScript)中常见的那种动态特性。例如,在 Objective-C 中,我们可以在运行时动态访问对象中的任何属性和方法,甚至交换、修改其相关的实现。
想要了解更多 Swift 动态机制内容的小伙伴们,请移步如下链接观赏精彩的内容:
- 『番外篇二』Swift “黑魔法”之动态获取类实例隐藏属性的值
- SwiftUI 利用 Swizz 黑魔法为系统创建的默认对象插入新协议方法(五)
- SwiftUI 利用 Swizz 黑魔法为系统创建的默认对象插入新协议方法(六)
- SwiftUI 如何在运行时从底层动态获取任何 NSObject 对象实例
- 『番外篇五』SwiftUI 进阶之如何动态获取任意视图的 tag 和 id 值
虽然这种缺乏动态性的特点可能恰恰是 Swift 如此强大的主要缘由,因为它可以帮助我们编写可预测性更强的逻辑,并且有更大的概率撸出“正确”的代码。
不过,有时能够以更动态的方式处理我们的实现夙愿也会非常有用。
谢天谢地!进化中的 Swift 语言不断汲取着越来越多本质上更动态的功能,同时也仍然专注于类型的安全性,这其中一个不可或缺的特性便是键路径(Key Paths)。
在 Swift 中广义的键路径是指一种动态访问和修改对象属性的机制,而狭义的键路径则用来表示特定根类型上特定属性值的类型。
一般来说存在三种键路径:
- KeyPath: 最常见的形式,用来提供到某一类型特定属性的只读路径;
- WritableKeyPath: 用值语义(value semantics)提供到某一类型特定属性的读写路径(因而,该类型的实例也必须是可写的);
- ReferenceWritableKeyPath: 和 WritableKeyPath 类似,不过只能用在引用类型上(比如类);
除了上面最常见的三种键路径类型以外,还有其它一些键路径。它们主要被用于减少内部代码重复或帮助类型抹除(Type erase)等情况,限于篇幅就不在本文中介绍了,
如果想要进一步了解这些额外键路径类型,请小伙伴们移步如下链接观赏进一步的内容:
在初步了解键路径的基本概念之后,下面就让我们深入探寻一番如何使用关键路径,以及它们因何而有趣、又因何而强大吧。
2. 功能快速简化之妙用
假设我们正在构建一款应用程序,它允许用户阅读来自网页的内容。我们设计了一个 Article 模型用来表示 Web 页面中对应的文章,如下所示:
struct Article { let id: UUID let source: URL let title: String let body: String }
在大多数情况下,每当我们使用这样的模型数组时,通常希望从数组每个元素中提取一块数据以形成新的数组 —— 例如,在下面两个示例中我们从一组文章(Article)中收集了所有的 id 和 source:
let articleIDs = articles.map { $0.id } let articleSources = articles.map { $0.source }
虽然上面的代码并没有什么错,不过由于我们的愿望只是单纯地从数组元素的单个属性中提取值,所以使用闭包看似有些大材小用了。
在这里,换为键路径会更加恰如其分。
extension Sequence { func map<T>(_ keyPath: KeyPath<Element, T>) -> [T] { return map { $0[keyPath: keyPath] } } }
如上代码所示,我们为序列(Sequence)增加了一个协议扩展方法 map,该方法的参数为序列元素任意属性的键路径。我们在 map 方法的实现中巧妙利用 Swift 下标( subscript)语法糖“有胆有识”的访问了序列元素属性的值。
这样一来,我们即可以用非常 Nice 的语法来提取序列元素任意属性的内容啦!所以之前的代码可以重构为如下形式了:
let articleIDs = articles.map(\.id) let articleSources = articles.map(\.source)
虽然这让秃头码农们觉得很酷,不过键路径真正熠熠生辉的地方是当它们用于构建更复杂表达式的时候:比如在对数组排序时。
众所周知,Swift 标准库能够自动对包含 Sortable 元素的任何序列进行排序,但对于所有其它类型,我们必须提供自己的排序闭包。然而,使用键路径我们也可以轻而易举地添加对基于可比较键路径序列元素进行排序的支持。
与之前类似,我们将在 Sequence 协议上添加一个扩展方法,它的作用是将给定的键路径转换为排序表达式闭包:
extension Sequence { func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] { return sorted { a, b in return a[keyPath: keyPath] < b[keyPath: keyPath] } } }
有了上面的“铺垫”,我们现在只需给出想要排序的键路径,即可优哉游哉地对任何类型元素的序列进行排序啦。
假若我们正在构建的 App 需要处理任何形式的可排序列表,例如包含播放列表的音乐应用程序 —— 这将非常方便,因为我们现在可以根据任何可比较属性(甚至嵌套属性)对列表进行排序了:
playlist.songs.sorted(by: \.name) playlist.songs.sorted(by: \.dateAdded) playlist.songs.sorted(by: \.ratings.worldWide)
上面代码看起来就像在优雅地添加“甜美的”语法糖,这既可以使处理序列复杂的代码更容易阅读,也可以帮助减少代码重复(DRY):因为小伙伴们现在可以“为所欲为”的向任何属性重用相同的排序逻辑啦,很棒哦!
3. 将键路径当做方法传递
一个好消息是:从 Swift 5.2 开始,上面 Sequence 扩展中的 map 方法已不再需要,因为键路径如今可以自动地转换为方法啦(converted into functions)!
这可能只是 Swift 语言进化中的一小步,但却是键路径“功成名就”的一大步!因为它会使序列上功能闭包的调用看起来更加“青出于蓝” —— 因为我们现在可以直接传递该属性的键路径了:
struct Movie { var name: String var isFavorite: Bool ... } let movies: [Movie] = loadMovies() // 等价于 movies.map { $0.name } let movieNames = movies.map(\.name) // 等价于 movies.filter { $0.isFavorite } let favoriteMovies = movies.filter(\.isFavorite)
总结
在本篇博文中,我们先是介绍了 Swift 语言中“简约却不简单”的键路径(Key Paths)机制,接着讨论了将它用来简化逻辑以及当成方法(functions)传递的美妙瞬间。
我们将在下一篇博文中继续介绍如何用键路径超越对象实例,特例化(specialize)数据模型;以及用可写键路径彻底摆脱“引用循环”,让简化代码“一蹴而就”。
感谢观赏,下一篇再会喽!😎