[譯]Swift 結構體指針

結構體指針

全部的代碼均可以在 gist 上獲取。前端

最近我打算爲 Swift 的最新的 keypaths 找一個好的使用場景,這篇文章介紹了我意外得到的一個使用示例。這是我剛研究出來的,但還沒實際應用在生產代碼上的成果。也就是說,我只是以爲這個成果很是酷並想把它展現出來。react

思考一個簡單的通信錄應用,這個應用包含一個展現聯繫人的列表視圖和展現聯繫人實例的詳情視圖控制器。若是把 定義成一個類的話,大概是這個樣子:android

class Person {
    var name: String
    var addresses: [Address]
    init(name: String, addresses: [Address]) {
        self.name = name
        self.addresses = addresses
    }
}

class Address {
    var street: String
    init(street: String) {
        self.street = street
    }
}複製代碼

咱們的(假設)viewController 有一個經過初始化方法設置的 person 屬性。這個類還有一個 change 方法來修改這我的的屬性。ios

final class PersonVC {
    var person: Person
    init(person: Person) {
        self.person = person
    }

    func change() {
        person.name = "New Name"
    }
}複製代碼

讓咱們思考下當 Person 初始化爲一個對象後遇到的問題:git

  • 由於 person 是一個指針,其餘部分的代碼就可能修改它。這是很是實用的,由於這讓消息傳遞成爲了可能。而與此同時,咱們須要保證咱們能夠一直監聽的到這些改變(好比使用 KVO ),不然咱們可能會遇到數據不一樣步的問題。但保證咱們可以實時監聽則是不容易實現的。
  • 當地址發生變化時,收到通知就更難了。觀察嵌套的對象屬性則是最困難的。
  • 若是咱們須要給 Person 建立一個獨立的本地 copy,咱們就須要實現一些像 NSCopying 這樣的東西, 這須要很多的工做量。甚至當咱們決定這麼作時,咱們仍然不得不考慮是想要深拷貝(地址也被拷貝)仍是淺拷貝(地址數組是獨立的,可是裏面的地址仍指向相同的對象)?
  • 若是咱們把 Person 當成 AddressBook 數組的元素,咱們可能想要知道通信錄何時作了修改(好比說進行排序)。而想要知道你的對象圖中的東西什麼時候作了改變要麼須要大量的樣板,要麼須要大量的觀察。

若是 PersonAddress 作成結構體的話,咱們又會碰到不一樣的問題:github

  • 每一個結構體都是獨立的拷貝。這是有用的,由於咱們知道它老是一致的,不會在咱們手底下改變。然而,當咱們在詳情控制器 中對 Person 作了修改時。咱們就須要一個方法來將這些改變反饋給列表視圖(或者說通信錄列表)。而對於對象,這種狀況會自動發生(經過在適當的位置修改 Person )。
  • 咱們能夠觀察通信錄結構體的根地址,從而知道通信錄發生的任何變化。然而,咱們仍是不能很容易得觀察到它內部屬性的變化(好比:觀察第一我的的名字)。

我如今提出的解決方案結合了兩個方案的最大優點:數據庫

  • 咱們有可變的共享指針
  • 由於底層數據是結構體,因此咱們能夠隨時獲得咱們本身的獨立拷貝
  • 咱們能夠觀察任何部分:不管在根級別,仍是觀察獨立的屬性(例如第一我的的名字)

我接下來會演示這個方案怎麼使用,如何工做,最後再說說方案的侷限性和問題。編程

讓咱們用結構體來建立一個通信錄。swift

struct Address {
    var street: String
}
struct Person {
    var name: String
    var addresses: [Address]
}

typealias Addressbook = [Person]複製代碼

如今咱們可使用咱們的 Ref 類型( Reference 的簡稱)。
咱們用一個初始化的空數組來建立一個新的 addressBook。而後添加一個 Person 。接下來就是最酷的地方:經過使用下標咱們能夠得到指向第一我的的 指針 ,接着是一個指向他們名字的 指針 。咱們能夠將指針指向的內容改成 「New Name" 來驗證咱們是否更改了原始的通信錄。後端

let addressBook = Ref<Addressbook>(initialValue: [])
addressBook.value.append(Person(name: "Test", addresses: []))
let firstPerson: Ref<Person> = addressBook[0]
let nameOfFirstPerson: Ref<String> = firstPerson[\.name]
nameOfFirstPerson.value = "New Name"
addressBook.value // shows [Person(name: "New Name", addresses: [])]複製代碼

firstPersonnameOfFirstPerson 類型能夠被忽略,它們僅僅是爲了增長代碼可讀性。

不管什麼時候咱們均可以對 Person 內容進行獨立備份。一旦你作了拷貝,咱們就可使用 myOwnCopy ,而且沒必要實現 NSCopying 就能保證它的內容不會在咱們手底下改變:

var myOwnCopy: Person = firstPerson.value複製代碼

咱們能夠監放任何 Ref 。就像 reactive 庫同樣,咱們獲得了一個能夠控制觀察者生命週期的一次性調用:

var disposable: Any?
disposable = addressBook.addObserver { newValue in
    print(newValue) // Prints the entire address book
}

disposable = nil // stop observing複製代碼

咱們也能夠監聽 nameOfFirstPerson 。在目前的實現中,不管何時通信錄中的任何改變都會觸發監聽,但之後的實現會有更多的功能。

nameOfFirstPerson.addObserver { newValue in
    print(newValue) // Prints a string
}複製代碼

讓咱們返回咱們的 PersonVC 。咱們可使用 Ref 做爲他的實現。 這樣 viewController 就能夠收到每一次更改。在響應式編程中,信號一般是隻讀類型的(你只會收到發生了變化的信息),這時你就須要找到另外一種回傳信號的方法。 在 Ref 方案中,咱們可使用 person.value 進行回寫:

final class PersonVC {
    let person: Ref<Person>
    var disposeBag: Any?
    init(person: Ref<Person>) {
        self.person = person
        disposeBag = person.addObserver { newValue in
            print("update view for new person value: \(newValue)")
        }
    }

    func change() {
        person.value.name = "New Name"
    }
}複製代碼

這個 PersonVC 不知道 Ref <Person>是從哪裏得到的:是從一個 person 數組,一個數據庫或者其餘地方。實際上,咱們能夠經過將咱們的數組包裝在 History 結構體 中來撤銷對咱們通信錄的支持。
這樣咱們就再也不須要修改 PersonVC

let source: Ref<History<Addressbook>> = Ref(initialValue: History(initialValue: []))
let addressBook: Ref<Addressbook> = source[\.value]
addressBook.value.append(Person(name: "Test", addresses: []))
addressBook[0].value.name = "New Name"
print(addressBook[0].value)
source.value.undo()
print(addressBook[0].value)
source.value.redo()複製代碼

咱們還能夠爲它添加其餘的不少東西:緩存,序列化,自動同步(好比只在子線程上修改和觀察),但這都是以後的工做。

實現細節

咱們來看看這個事情是如何實現的。咱們首先從 Ref 類的定義開始。
Ref 包含一個獲取值和一個設置值的方法,以及添加一個觀察者的方法。它有一個須要三個參數的初始化方法:

final class Ref<A> {
    typealias Observer = (A) -> ()

    private let _get: () -> A
    private let _set: (A) -> ()
    private let _addObserver: (@escaping Observer) -> Disposable

    var value: A {
        get {
            return _get()
        }
        set {
            _set(newValue)
        }
    }

    init(get: @escaping () -> A, set: @escaping (A) -> (), addObserver: @escaping (@escaping Observer) -> Disposable) {
        _get = get
        _set = set
        _addObserver = addObserver
    }

    func addObserver(observer: @escaping Observer) -> Disposable {
        return _addObserver(observer)
    }
}複製代碼

如今咱們能夠添加一個能夠觀察單個結構體值的初始化方法。它建立了一個觀察者和變量對應的字典。這樣不管變量何時被修改了,全部的觀察者都會被通知到。它使用上述定義的初始化方法,並傳遞給 get, set, 和 addObserver:

extension Ref {
    convenience init(initialValue: A) {
        var observers: [Int: Observer] = [:]
        var theValue = initialValue {
            didSet { observers.values.forEach { $0(theValue) } }
        }
        var freshId = (Int.min...).makeIterator()
        let get = { theValue }
        let set = { newValue in theValue = newValue }
        let addObserver = { (newObserver: @escaping Observer) -> Disposable in
            let id = freshId.next()!
            observers[id] = newObserver
            return Disposable {
                observers[id] = nil
            }
        }
        self.init(get: get, set: set, addObserver: addObserver)
    }
}複製代碼

想一下咱們如今已經有 Person 指針,爲了拿到 Person name 屬性的指針,咱們須要一種方式來對 name 進行讀寫操做。而 WritableKeyPath 剛好能夠作到。所以,咱們能夠在 Ref 中添加一個subscript 來建立能夠指向 Person 某一部分的指針:

extension Ref {
    subscript<B>(keyPath: WritableKeyPath<A,B>) -> Ref<B> {
        let parent = self
        return Ref<B>(get: { parent._get()[keyPath: keyPath] }, set: {
            var oldValue = parent.value
            oldValue[keyPath: keyPath] = $0
            parent._set(oldValue)
        }, addObserver: { observer in
            parent.addObserver { observer($0[keyPath: keyPath]) }
        })
    }
}複製代碼

上面的代碼有一點難於理解,但若是隻是爲了使用這個庫,咱們不須要真的弄明白它是怎麼實現的。

也許某一天,Swift 中的 keypath 也會支持下標,但至少如今沒有,接下來咱們必須爲集合添加另一個下標。除了使用索引而不是 keypath ,它的實現幾乎就跟上面的同樣。

extension Ref where A: MutableCollection {
    subscript(index: A.Index) -> Ref<A.Element> {
        return Ref<A.Element>(get: { self._get()[index] }, set: { newValue in
            var old = self.value
            old[index] = newValue
            self._set(old)
        }, addObserver: { observer in
                self.addObserver { observer($0[index]) }
        })
    }
}複製代碼

這就是所有實現了。上面代碼使用了 Swift 大量新特性,但它仍保持在 100 行代碼如下。若是沒有 Swift 4 最新功能,這也基本不可能實現。它依賴於 keypaths ,通用下標,開放範圍以及之前在 Swift 中提供的許多功能。

討論

就如以前所提到的那樣,這些仍處於研究中而不是生產級的代碼。一旦我開始在一個真正的應用程序中使用它,我很是感興趣想知道未來會遇到什麼樣問題。 下面就是其中一個讓我感到困惑的代碼段::

var twoPeople: Ref<Addressbook> = Ref(initialValue:
    [Person(name: "One", addresses: []),
     Person(name: "Two", addresses: [])])
let p0 = twoPeople[0]
twoPeople.value.removeFirst()
print(p0.value) // what does this print?複製代碼

我頗有興趣將它更進一步。我甚至能夠想象的到,若是我爲他添加隊列支持,你就能夠像下面那樣使用:

var source = Ref<Addressbook>(initialValue: [],
    queue: DispatchQueue(label: "private queue"))複製代碼

我還能想象的到你能夠用它和數據庫搭配使用。這個 Var 將會讓你同時支持讀寫操做,並訂閱任何修改的通知:

final class MyDatabase {
   func readPerson(id: Person.Id) -> Var<Person> {
   }
}複製代碼

我期待着聽到您的評論和反饋,若是你須要更深刻的理解它是如何工做的,試着本身去實現它(即使你已經看了代碼)。順便提一下,咱們將會以它爲主題開展兩場 Swift Talk。若是你對 Florian 和我從頭開始構建這個項目感興趣,就訂閱它吧。

更新: 感謝 Egor Sobko 指出了一個微妙但卻相當重要的錯誤:我爲觀察者發送的是 initialValue 而不是 theValue,已修改!


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索