SE-0227 中描述的 KeyPath 是 Swift 5 加入的一个特性。它可以让我们把一个对象的属性,当成另外一个独立的值使用。这个值可以作为参数,可以用在表达式里。这也就意味着,我们甚至可以在不知道一个属性所属的具体类型的前提下,就对其进行赋值。在 Swift 5.2 里,SE-0249 还让 KeyPath 自身可以作为一个函数,传递给高阶函数作为谓词使用。
那么为什么现在要提到这个特性呢?用一句话表达就是:我们期望用一个泛型函数对从 Core Data 读取出来的结果集合按照其元素的某种属性进行排序。例如下面这样:
private func loadRecentUpdates() -> [Episode] {
return load(by: \.updatedAt, criteria: >)
}
它可以按照更新时间对从 Core Data 取出来的最近更新视频记录进行排序。而 load
的实现,就会用到 KeyPath。因此,如果你还不熟悉 KeyPath,这一节,我们用最简单的例子讲下这三种不同的类型:KeyPath / WritableKeyPath / ReferenceWritableKeyPath
。如果你已经了解它们,就可以直接跳到下一节了。
KeyPath
首先,我们从最简单的 KeyPath
类型说起。现在,假设有一个表示视频的值类型:
struct Episode {
let id: Int
let title: String
}
let first = Episode(id: 1, title: "Episode1")
现在,为了访问 first.id
,除了直接用 .
之外,我们还可以这样:
let id = first[keyPath: \Episode.id]
print(type(of: \Episode.id)) // KeyPath<Episode, Int>
first[keyPath: \Episode.id]
和 first.id
的结果,是相同的。这里,\Episode.id
就是 id
这个属性的 KeyPath,当然,如果编译器可以根据上下文推断出 id
所属的类型,也可以进一步简写成 \.id
。它的类型是 KeyPath<Episode, Int>
,第一个泛型参数表示要引用的类型,第二个泛型参数表示其中要指定的属性的类型。
看到这,如果你有一些 Objective-C 的编程经验一定会想,尽管 id
是一个常量无法直接赋值,但我能用 KeyPath 间接修改它么:
first[keyPath: \Episode.id] = 2
答案是不行。编译器会提示你:Cannot assign through subscript: key path is read-only。即便我们把 Episode
改成这样:
struct Episode {
var id: Int
/// ...
}
编译器还是会给出同样的提示,因为 first
还是一个常量。这和 Swift 中值类型的可修改机制是完全一样的。
WritableKeyPath
因此,为了通过 keypath 修改 id
,我们还需要让 first
从常量变成变量:
var first = Episode(id: 1, title: "Episode1")
此时 \Episode.id
的类型就变成了 WritableKeyPath<Episode, Int>
。
ReferenceWritableKeyPath
接下来,我们把这个例子改成这样:
class Episode {
var id: Int
let title: String
init(id: Int, title: String) {
self.id = id
self.title = title
}
}
let first = Episode(id: 1, title: "Episode1")
此时,尽管 first
是一个常量。我们还是可以用 first.id
的形式修改 id
属性。因此,按道理说,KeyPath 同样应该可以:
first[keyPath: \Episode.id] = 2
只是这次,\Episode.id
的类型,就变成了 ReferenceWritableKeyPath<Episode, Int>
。那么,如果把 id
再改回常量呢?这样,即便 Episode
是一个引用类型,first.id
也不可修改了。此时 Episode.id
就会变回 KeyPath<Episode, Int>
。
What's next?
看到这,你可能会觉得,这有什么用呢?除了访问属性更麻烦了之外,似乎没什么用处。的确,如果只是单纯用 KeyPath 访问对象属性,的确没什么大用。KeyPath 真正有用的地方在于,它可以让我们把类对象属性的引用和具体的类对象剥离开。下一节,我们就基于这个特性,为 Sequence
编写一个有用的扩展。简化从 Core Data 中读取记录时,对结果的搜索。