KeyPath 最佳實踐

Swift 4.0 帶來的一個新功能就是 Smart KeyPath,以前在 Twitter 上看到 Chris Eidhof 大神在徵集 KeyPath 的用法。html

我也蒐集了一下,看成是一次總結,這裏面的技巧其實大部分都很難在實踐中用上,只是好玩有趣而已,也算是一種啓發吧。git

類型安全的 Query API

出處: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 正式化的需求,不過暫時仍是不推薦你們使用這種方式。閉包

ReadOnly 的 Class

出處: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

不過這種只讀權限的顆粒度太大,只能細緻到整個類實例,而不能針對每個屬性。並且我在實踐中也沒有找到合適的使用場景。

取代 Selector 的抽象

這是我在泊學網的會員羣裏偶然看到的,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 很接近,因此後面會兩個功能一塊兒實現。

狀態共享的值類型

出處:Swift Talk #61 | Swift Talk #62

這應該算是這篇文章裏面最 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複製代碼

上面咱們能夠看到 ageVarjohnVar 分割出來以後,它的狀態依舊跟 johnVar 保持一致,這是由於 Var 的 init 方法裏使用 block 捕獲了 x 這個變量,也就至關於做爲 inout 參數傳入了進去,這個時候 x 會存放在堆區。

而且使用 subscript 生成了 ageVar 以後,ageVar 使用的 init 的方法只是在本來的 _get_set 方法外面再包了一層,因此 ageVar 修改值的時候,也是使用了本來 johnVar 同樣的 _set,修改了最初 johnVar 初始化時使用的 x。換句話說,ageVarjohnVar 使用的都是堆區裏同一個 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 的使用很是重要,由於它能夠經過類型的結構,去獲取任意一個實例的相應屬性,並且這種方式遠比閉包更加簡單和緊湊。

以爲文章還不錯的話能夠關注一下個人博客

相關文章
相關標籤/搜索