可变共享结构(第一部分)

作者: iOS_小久 | 来源:发表于2019-07-05 19:54 被阅读80次

    今天我们将在结构和类之间构建一个新类型,它包含了这两个方面的积极方面,包括对象的共享状态,知道结构中任何地方发生变化的可能性,以及在任何一点制作私人副本。

    这是一个实验。我们不确定最终结果是否有用,但至少我们可以使用一些不错的Swift功能并突破语言的极限。

    小编这里推荐一个群:691040931 里面有大量的书籍和面试资料哦有技术的来闲聊 没技术的来学习

    使用班级

    让我们先来看一个展示我们所面临的挑战的例子。我们有一个Person类,我们创建了一个包含一些实例的数组:

    final class Person {
    var first: String
    var last: String

    init(first: String, last: String) {
        self.first = first
        self.last = last
    }
    

    }

    let people = [
    Person(first: "Jo", last: "Smith"),
    Person(first: "Joanne", last: "Williams"),
    Person(first: "Annie", last: "Williams"),
    Person(first: "Robert", last: "Jones")
    ]`

    假设我们在带有表视图控制器的iOS应用程序中使用它,它允许我们在详细视图控制器中查看单个项目,也许详细视图控制器想要更新对象:

    final class PersonViewController {
    var person: Person

    init(person: Person) {
        self.person = person
    }
    
    func update() {
        person.last = "changed"
    }
    

    }

    当我们初始化a PersonViewController并传入people数组的第一个元素然后调用update方法时,我们正在改变Person详细视图控制器所持有的那个。使用对象的好处是Person对详细视图控制器的更改将反映在第一个Person人员数组中:

    let vc = PersonViewController(person: people[0])
    vc.update()

    dump(people[0])
    // - last: "changed"

    除非我们想观察变化并做出反应,否则我们不必担心沟通变化。数组本身永远不会更改,因为它只保存对象的引用。该Person实例本身可能会改变,但他们的人数组引用保持不变。我们people用a 定义了这个事实已经暗示了这一点let

    使用结构

    如果Person是一个结构,我们将people用a 定义var,然后我们就能观察到变化。但既然Person是一个类,一个didSetpeople不叫,因为价值people变量没有改变:

    var people = [
    Person(first: "Jo", last: "Smith"),
    Person(first: "Joanne", last: "Williams"),
    Person(first: "Annie", last: "Williams"),
    Person(first: "Robert", last: "Jones")
    ] {
    didSet {
    dump("people didSet (people)")
    }
    }

    使用结构时,如果我们想要观察数据结构,我们可以利用值语义:通过观察变量,我们可以在结构中的任何地方通知变化。但是如果我们想要观察一组对象,我们要么必须观察每个对象,要么我们必须非常自律地回复我们对任何对象所做的任何更改。

    我们转换Person为结构,看看我们必须对代码进行哪些更改。一个很好的副作用是我们不再需要编写标准初始值设定项:

    struct Person {
    var first: String
    var last: String
    }

    如果我们真的改变它的价值didSetpeople那么现在会被召唤。但是我们将第一个人的副本而不是引用传递给详细视图控制器,因此详细视图控制器正在更新其自己的副本Personpeople只有在我们直接在数组中进行更改时才会调用观察者:

    people[0].last = "new"
    // prints "people didSet [Person(first:"Joe", last: "new"), ...]"

    为结构创建新变量时,您正在创建副本。对副本的更改不会影响people数组:

    var personZero = people[0]
    personZero.last = "new"
    // people[0].last: "Smith"

    现在我们正在处理结构,我们必须以某种方式将此更改传回原始people数组,例如通过使用委托或回调。

    瓦尔

    我们将创建一个基本上是包含结构的框的类。我们把这个名字命名Var为缺少一个更好的名字。在里面Var,我们可以观察结构的值didSet。这样我们就可以在任何地方使用对框的引用,并且仍然有一个中心didSet来跟踪它的变化。

    Var下课其所含值通用的,它需要一个观察者关闭,我们挂接到价值的didSet

    final class Var<A> {
    var value: A {
    didSet {
    observer(value)
    }
    }
    var observer: (A) -> ()
    init(initialValue: A, observe: @escaping (A) -> ()) {
    self.value = initialValue
    self.observer = observe
    }
    }

    我们Var用people数组创建一个并尝试更新数组的第一个元素。这会导致数组被转储到控制台:

    let peopleVar = Var(initialValue: people) { newValue in
    dump(newValue)
    }
    peopleVar.value[0].last = "Test"

    接下来我们希望能够取出结构的一部分。假设我们想要关注第一个Person,就像在我们的原始示例中使用详细视图控制器一样。从peopleVar,我们想要创建另一个Var仅引用第一个Person。当我们改变这个新的值时Var,它应该更新原始值peopleVar

    最后,我们想要索引一个人数组,但我们首先使用Swift 4 keypath下标Var从a中提取名字的第一个或最后一个名字Var<Person>。当它工作时,我们将根据密钥路径实现添加数组下标。

    11:27所以我们从Var一个单一开始Person

    let personVar: Var<Person> = Var(initialValue: people[0]) { newValue in
    dump(newValue)
    }

    为了从中提取Var第一个名字personVar,我们在Var类上添加一个带有关键路径的下标。关键路径是WritableKeyPath因为我们想要读取和写入它。此外,密钥路径有两个通用参数:我们正在下载的基类型(我们的泛型类型A)和返回值(我们将调用它B)。下标将返回一个新的Var<B>

    final class Var<A> {
    // ...

    subscript<B>(keyPath: WritableKeyPath<A, B>) -> Var<B> {
    
    }
    

    }

    在身体中我们必须返回一个Var<B>,所以我们将开始创建它。我们使用标准键路径下标设置其初始值。在观察者中,我们使用相同的键路径获取新值并将其写回我们自己的值:

    final class Var<A> {
    var value: A // ...
    // ...

    subscript<B>(keyPath: WritableKeyPath<A, B>) -> Var<B> {
        return Var<B>(initialValue: value[keyPath: keyPath]) { newValue in
            self.value[keyPath: keyPath] = newValue
        }
    }
    

    }

    我们来试试吧。我们创建了Var第一个名称personVar并更改其值:

    let firstNameVar: Var<String> = personVar[.first]

    firstNameVar.value = "new first name"

    更改值会触发原始观察者,personVar我们会看到打印出的新名字。所以它几乎可以工作,但它不起作用; 通过值更改名字personVar不会更新以下值firstNameVar

    let firstNameVar: Var<String> = personVar[.first]

    personVar.value.first = "new first name"
    // firstNameVar.value: "Jo"

    我们的下标实施是错误的。我们正在捕获初始值,但我们应该捕获对该值的引用。我们会做一招,隐藏valueobserverVar其初始内部:

    final class Var<A> {
    init(initialValue: A, observe: @escaping (A) -> ()) {
    var value = initialValue {
    didSet {
    observe(value)
    }
    }
    }

    subscript<B>(keyPath: WritableKeyPath<A, B>) -> Var<B> {
        // ...
    }
    

    }

    现在我们只能在初始化器中指定初始值和观察者。我们仍然希望稍后获取或设置该值,因此我们将使用value带有getter和setter 的计算属性,这两者都是存储属性:

    final class Var<A> {
    private let _get: () -> A
    private let _set: (A) -> ()
    var value: A {
    get {
    return _get()
    }
    set {
    _set(newValue)
    }
    }
    init(initialValue: A, observe: @escaping (A) -> ()) {
    var value = initialValue {
    didSet {
    observe(value)
    }
    }
    _get = { value }
    _set = { newValue in value = newValue }
    }

    subscript<B>(keyPath: WritableKeyPath<A, B>) -> Var<B> {
        // ...
    }
    

    }

    这段代码有点棘手。在初始化程序中定义getter的那一刻,指定的闭包捕获对value变量的引用。因此,当我们通过计算属性调用getter时,我们实际上使用引用来检索值。

    现在我们可以编写一个私有初始化程序,它接受一个getter和一个setter。我们将这个初始化器用于下标实现,对于getter和setter,我们value使用给定的键路径调用computed属性:

    final class Var<A> {
    private let _get: () -> A
    private let _set: (A) -> ()
    // ...

    private init(get: @escaping () -> A, set: @escaping (A) -> ()) {
        _get = get
        _set = set
    }
    
    subscript<B>(keyPath: WritableKeyPath<A, B>) -> Var<B> {
        return Var<B>(get: {
            self.value[keyPath: keyPath]
        }, set: { newValue in
            self.value[keyPath: keyPath] = newValue
        })
    }
    

    }

    让我们看看这个行动。如果我们通过改变名字personVar,我们也会看到更改反映在firstNameVar

    let personVar: Var<Person> = Var(initialValue: people[0]) { newValue in
    dump(newValue)
    }

    let firstNameVar: Var<String> = personVar[.first]

    personVar.value.first = "new first name"
    firstNameVar.value // "new first name"

    另一种方式也适用:

    firstNameVar.value = "test"
    personVar.value.first // "test"

    我们已经实现了一种创建对可观察结构值的引用的方法,我们可以创建对它的一部分的引用。奇怪的是,我们重新创造了对象的概念,并添加了内置的观察机制。

    索引下标

    为了使我们的原始示例能够工作Var,我们需要能够下标到数组中。所以我们需要另一个与集合值一起使用的下标。理论上,我们的关键路径下标也应该支持集合,但Swift 4的关键路径部分尚未实现。

    相反,我们将创建一个变通方法,并Var在其包含集合的位置添加下标。我们不需要追加或删除元素; 我们只需要通过索引获取和设置元素。所以我们可以将下标约束到MutableCollection协议:

    extension Var where A: MutableCollection {

    }

    新的下标采用一个索引,我们可以使用该索引的索引类型,并返回一个Var包含该集合元素的索引:

    extension Var where A: MutableCollection {
    subscript(index: A.Index) -> Var<A.Element> {

    }
    

    }

    这个下标的实现与我们的其他下标方法非常相似,所以我们可以遵循相同的方法,并通过调用集合的索引下标来替换键路径下标。我们需要扩展getter / setter初始化程序的访问级别,fileprivate以便从扩展程序中使用它:

    extension Var where A: MutableCollection {
    subscript(index: A.Index) -> Var<A.Element> {
    return Var<A.Element>(get: {
    self.value[index]
    }, set: { newValue in
    self.value[index] = newValue
    })
    }
    }

    现在我们可以定义一个peopleVar并从中personVar取出它:

    let peopleVar: Var<[Person]> = Var(initialValue: people) { newValue in
    dump(newValue)
    }

    let personVar: Var<Person> = peopleVar[0]

    改变personVar现在触发观察者peopleVar。关于另一个方向,peopleVar也可以看到突变personVar

    使用Var

    我们回到我们原来的代码和应用VarPersonViewController。这样,视图控制器可以更新其模型,并将这些更改反映回people变量:

    final class PersonViewController {
    let person: Var<Person>

    init(person: Var<Person>) {
        self.person = person
    }
    
    func update() {
        person.value.last = "changed"
    }
    

    }

    let peopleVar: Var<[Person]> = Var(initialValue: people) { newValue in
    dump(newValue)
    }

    let vc = PersonViewController(person: peopleVar[0])
    vc.update()

    让我们回顾一下这里发生的事情。我们peopleVar用一组Person结构创建一个。我们将第一个人传递给视图控制器。视图控制器的值更新被引用回原始peopleVar数组。最后,我们将控制台中的“已更改”视为姓氏。

    如果视图控制器仍然需要其模型的独立副本,它可以通过使用Var的值轻松获得它:

    let independentCopy = person.value

    PersonViewController不知道一个人的阵列任何东西; 它只是收到一个Var<Person>可以读取和写入的内容。这就是它所需要的一切。

    去做

    缺少一件事是能够观察变量。现在,我们只有root变量的初始化器,我们可以在其中定义一个观察者。PersonViewController对于观察和改变它的变化是有用的person。添加该功能将是一项工作


    扫码进交流群 有技术的来闲聊 没技术的来学习

    691040931

    原文转载地址:https://talk.objc.io/episodes/S01E61-mutable-shared-structs-part-1

    相关文章

      网友评论

        本文标题:可变共享结构(第一部分)

        本文链接:https://www.haomeiwen.com/subject/izbehctx.html