Swift 4.0 帶來的一個新功能就是 Smart KeyPath,以前在 Twitter 上看到 Chris Eidhof 大神在徵集 KeyPath 的用法。html
我也蒐集了一下,看成是一次總結,這裏面的技巧其實大部分都很難在實踐中用上,只是好玩有趣而已,也算是一種啓發吧。git
出處:Kuerygithub
利用了 KeyPath 類型安全的特性,提供了類型安全的 Query API。目前惟一作出來的一個成品是 Kuery 庫,類型安全的 CoreData 查詢 API,相同的方式也能夠爲 Realm,SQLite 等數據庫服務,下面是它的使用範例:數據庫
Query(Person.self).filter(\.name != "Katsumi")
Query(Person.self).filter(\.age > 20)複製代碼
其實我我的以爲這個 API 還能夠再簡化:swift
Query.filter(\Person.name != "Katsumi")
// 或
Query<Person>.filter(\.name != "Katsumi")複製代碼
這個庫的原理是操做符重載,你們看一下函數聲明就能大概理解了:api
public func == <ManagedObject: NSManagedObject, Property: Equatable>( lhs: KeyPath<ManagedObject, Property?>, rhs: Property?)
-> NSPredicate<ManagedObject> { ... }複製代碼
具體實現的時候使用了 KeyPath
的屬性 _kvcKeyPathString
,這是爲了兼容 ObjectiveC 的 KVC 而存在的屬性,它並不是是一個公開的 API,在正式文檔或 Xcode 裏是查不到這個屬性的,具體的細節咱們能夠在 GitHub 上看到。安全
雖然查不到,但目前代碼裏是可使用這個屬性的(Xcode 9.0,Swift 4.0),Kuery 的做者也去 Rader 裏反饋了將這個 API 正式化的需求,不過暫時仍是不推薦你們使用這種方式。閉包
出處:Chris Eidhofapp
final class ReadOnly<T> {
private let value: T
init(_ value: T) {
self.value = value
}
subscript<P>(keyPath: KeyPath<T, P>) -> P {
return value[keyPath: keyPath]
}
}
import UIKit
let textField = UITextField()
let readOnlyTextField = ReadOnly(textFiled)
r[\.text] // nil
r[\.text] = "Test" // 編譯錯誤複製代碼
這是個很好玩的實現,正常來講咱們實現只讀,都是使用接口的權限設計,例如 private(set)
之類的作法,但這裏利用了 KeyPath
沒法修改值的特性實現了這一個功能,強行修改就會像上面那樣在編譯時就拋出錯誤。ide
不過這種只讀權限的顆粒度太大,只能細緻到整個類實例,而不能針對每個屬性。並且我在實踐中也沒有找到合適的使用場景。
這是我在泊學網的會員羣裏偶然看到的,11 說 Swift 4 裏也有原生的 Selector。仔細想了一下,就只有 KeyPath 了,實現出來大概會是這樣:
// 定義
extension UIControl {
func addTarget<T>( _ target: T, action: KeyPath<T, (UIControl) -> ()>,
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 github.com/apple/swift….
KeyPath 指向方法的這個 Feature,和 SE-0042 很接近,因此後面會兩個功能一塊兒實現。
這應該算是這篇文章裏面最 Tricky 可是也最有趣的一個用法了,我在看 Swift Talk 的時候,介紹的一種狀態共享的值類型,直接上代碼:
final class Var<A> {
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<Part>(_ kp: WritableKeyPath<A, Part>) -> Var<Part> {
return Var<Part>(
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 的使用很是重要,由於它能夠經過類型的結構,去獲取任意一個實例的相應屬性,並且這種方式遠比閉包更加簡單和緊湊。
以爲文章還不錯的話能夠關注一下個人博客