KeyPath 最佳實踐

四娘發表於2019-02-21

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<Person>.filter(.name != "Katsumi")複製程式碼

這個庫的原理是操作符過載,大家看一下函式宣告就能大概理解了:

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 Eidhof

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 無法修改值的特性實現了這一個功能,強行修改就會像上面那樣在編譯時就丟擲錯誤。

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

取代 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 的使用非常重要,因為它可以通過型別的結構,去獲取任意一個例項的相應屬性,而且這種方式遠比閉包更加簡單和緊湊。

覺得文章還不錯的話可以關注一下我的部落格