虽然键路径和函数类型引用有紧密的关系,但很不幸的是 Swift 中它们的语法是不同的。不过,Swift 团队表达过在未来的版本中,为函数类型引用也引入这种反斜杠语法的兴趣。
正如其名,键路径描述了一个值从根开始的层级路径。举例来说,在下面的 Person 和 Address 类型中,\Person.address.street 表达了一个人的街道住址的键路径:
struct Address {
var street: String
var city: String
var zipCode: Int
}
struct Person {
let name: String
var address: Address
}
let streetKeyPath = \Person.address.street
// Swift.WritableKeyPath<Person, Swift.String>
let nameKeyPath = \Person.name // Swift.KeyPath<Person, Swift.String>
extension NSObjectProtocol where Self: NSObject {
func bind<A, Other>(_ keyPath: ReferenceWritableKeyPath<Self,A>,
to other: Other,
_ otherKeyPath: ReferenceWritableKeyPath<Other,A>)
-> (NSKeyValueObservation, NSKeyValueObservation)
where A: Equatable, Other: NSObject
{
let one = observe(keyPath, writeTo: other, otherKeyPath)
let two = other.observe(otherKeyPath, writeTo: self, keyPath)
return (one,two)
}
}
现在,我们可构建两个不同的对象,Person 和 TextField,然后将 name 和 text 属性互相绑定:
final class Person: NSObject {
@objc dynamic var name: String = ""
}
class TextField: NSObject {
@objc dynamic var text: String = ""
}
let person = Person()
let textField = TextField()
let observation = person.bind(\.name, to: textField, \.text)
person.name = "John"
textField.text // John
textField.text = "Sarah"
person.name // Sarah
如果你很熟细函数式编程,可写键路径可能会让你想起函数式编程中的透镜 (lenses)。它们紧密相关:通过一个 WritableKeypath<Root, Value>,你可以创建一个 Lens<Root, Value>。透镜的概念在像是 Haskell 或 PureScript 这样的纯函数式语言中非常有用,不过因为 Swift 内建支持可变性,所以在 Swift 里它没有那么有用。
这几种键路径的层级结构现在是通过类的继承来实现的。理想状态下,这些特性应该由协议来完成,但是 Swift 的泛型系统还缺少一些使之可行的特性。这种类的层级有意地保持了对外不可见,这样以便于未来在更新时,现有的代码也不会被破坏。
我们前面也提到,键路径不同于函数,它们是满足 Hashable 的,而且在将来它们很有可能还会满足 Codable。这也是为什么我们强调 AnyKeyPath 和 (Any) -> Any 只是相似的原因。虽然我们能够将一个键路径转换为对应的函数,但是我们无法做相反的操作。
对比 Objective-C 的键路径
在 Foundation 和 Objective-C 中,键路径是通过字符串来建模的 (我们会将它们称为 Foundation 键路径,以区别 Swift 的键路径)。由于 Foundation 键路径是字符串,它们不含有任何的类型信息。从这个角度看,它们和 AnyKeyPath 类似。如果一个 Foundation 键路径拼写错误、没有正确生成、或者它的类型不匹配的话,程序可能会崩溃。(Swift 中的 #keyPath 指令对拼写错误的问题进行了一些改善,编译器可以检查特定名字所对应的属性是否存在。) Swift 的 KeyPath、WritableKeypath 和 ReferenceWritableKeyPath 从构造开始就是正确的:它们不可能被拼错,也不会有类型错误。