Swift 4.0 带来的一个新功能就是 Smart KeyPath,之前在 Twitter 上看到 Chris Eidhof 大神在征集 KeyPath 的用法。
我也搜集了一下,当作是一次总结,这里面的技巧其实大部分都很难在实践中用上,只是好玩有趣而已,也算是一种启发吧。
类型安全的 Query API
出处:Kuery
利用了 KeyPath 类型安全的特性,提供了类型安全的 Query API。目前唯一做出来的一个成品是 Kuery 库,类型安全的 CoreData 查询 API,相同的方式也可以为 Realm,SQLite 等数据库服务,下面是它的使用范例:
Query(Person.self).filter(\.name != "Katsumi")
Query(Person.self).filter(\.age > 20)
其实我个人觉得这个 API 还可以再简化:
Query.filter(\Person.name != "Katsumi")
// 或
Query.filter(\.name != "Katsumi")
这个库的原理是操作符重载,大家看一下函数声明就能大概理解了:
public func == (
lhs: KeyPath,
rhs: Property?)
-> NSPredicate { ... }
具体实现的时候使用了 KeyPath
的属性 _kvcKeyPathString
,这是为了兼容 ObjectiveC 的 KVC 而存在的属性,它并非是一个公开的 API,在正式文档或 Xcode 里是查不到这个属性的,具体的细节我们可以在 GitHub 上看到。
虽然查不到,但目前代码里是可以使用这个属性的(Xcode 9.0,Swift 4.0),Kuery 的作者也去 Rader 里反馈了将这个 API 正式化的需求,不过暂时还是不推荐大家使用这种方式。
ReadOnly 的 Class
出处:Chris Eidhof
final class ReadOnly {
private let value: T
init(_ value: T) {
self.value = value
}
subscript(keyPath: KeyPath) -> P {
return value[keyPath: keyPath]
}
}
import UIKit
let textField = UITextField()
let readOnlyTextField = ReadOnly(textFiled)
r[\.text] // nil
r[\.text] = "Test" // 编译错误
这是个很好玩的实现,正常来说我们实现只读,都是使用接口的权限设计,例如 private(set)
之类的做法,但这里利用了 KeyPath
无法修改值的特性实现了这一个功能,强行修改就会像上面那样在编译时就抛出错误。
不过这种只读权限的颗粒度太大,只能细致到整个类实例,而不能针对每一个属性。而且我在实践中也没有找到合适的使用场景。
取代 Selector 的抽象
这是我在泊学网的会员群里偶然看到的,11 说 Swift 4 里也有原生的 Selector。仔细想了一下,就只有 KeyPath 了,实现出来大概会是这样:
// 定义
extension UIControl {
func addTarget(
_ target: T,
action: KeyPath ()>,
for controlEvents: UIControlEvents)
{ ... }
}
// 调用
button.addTarget(self, action: \ViewController.didTapButton, for: .touchUpInside)
这样处理的话,didTapButton
方法甚至都不需要依赖于 Objective-C 的 runtime,只要能用 KeyPath 把方法取出来就行了。
但实际试了一下之后,发现并不可行,我就去翻了一下 KeyPath 的提案:
We think the disambiguating benefits of the escape-sigil would greatly benefit function type references, but such considerations are outside the scope of this proposal.
前半句其实我不太理解,但整句话读下来,感觉应该是实现起来很复杂,会与另外的一个问题交织在一起,所以暂时不在这个提案里处理。我去翻邮件列表的时候终于找到了想要的答案:
for unapplied method references, bringing the two closely-related features into syntactic alignment over time and providing an opportunity to stage in the important but currently-source-breaking changes accepted in SE-0042 https://github.com/apple/swift-evolution/blob/master/proposals/0042-flatten-method-types.md.
KeyPath 指向方法的这个 Feature,和 SE-0042 很接近,所以后面会两个功能一起实现。
状态共享的值类型
出处:Swift Talk #61 | Swift Talk #62
这应该算是这篇文章里面最 Tricky 但是也最有趣的一个用法了,我在看 Swift Talk 的时候,介绍的一种状态共享的值类型,直接上代码:
final class Var {
private var _get: () -> A
private var _set: (A) -> ()
var value: A {
get { return _get() }
set { _set(newValue) }
}
init(_ value: A) {
var x = value
_get = { x }
_set = { x = $0 }
}
private init(get: @escaping () -> A, set: @escaping (A) -> ()) {
_get = get
_set = set
}
subscript(_ kp: WritableKeyPath) -> Var {
return Var(
get: { self.value[keyPath: kp] },
set: { self.value[keyPath: kp] = $0 })
}
}
看完代码可能有点难理解,我们再看一下示例然后再解释:
var john = Person(name: "John", age: 11)
let johnVar = Var(john)
let ageVar = johnVar[\.age]
print(johnVar.value.age) // 11
print(ageVar.value) // 11
ageVar.value = 22
print(johnVar.value.age) // 22
print(ageVar.value) // 22
johnVar.value.age = 33
print(johnVar.value.age) // 33
print(ageVar.value) // 33
上面我们可以看到 ageVar
从 johnVar
分割出来之后,它的状态依旧跟 johnVar
保持一致,这是因为 Var
的 init 方法里使用 block 捕获了 x
这个变量,也就相当于作为 inout 参数传入了进去,这个时候 x
会存放在堆区。
并且使用 subscript 生成了 ageVar
之后,ageVar
使用的 init 的方法只是在原本的 _get
和 _set
方法外面再包了一层,所以 ageVar
修改值的时候,也是使用了原本 johnVar
一样的 _set
,修改了最初 johnVar
初始化时使用的 x
。换句话说,ageVar
和 johnVar
使用的都是堆区里同一个 x
。听着是不是很像 class?
更具体的细节,大家可以去看 Swift Talk。
结尾
KeyPath is incredibly important in Cocoa Development. And this is they let us reason about the structure of our types apart from any specific instance in a way that's far more constrained than a closure.
—— What's New in Foundation · WWDC 2017 · Session 212
上面这段话摘录自今年 WWDC 的 What's New in Foundation,简单的翻译就是 KeyPath 对于 Cocoa 的使用非常重要,因为它可以通过类型的结构,去获取任意一个实例的相应属性,而且这种方式远比闭包更加简单和紧凑。
觉得文章还不错的话可以关注一下我的博客