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

96 阅读10分钟
原文链接: www.jianshu.com

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

这是一个实验。我们不确定最终结果是否有用,但至少我们可以使用一些不错的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

原文转载地址:talk.objc.io/episodes/S0…