可变共享结构(第一部分)
今天我们将在结构和类之间构建一个新类型,它包含了这两个方面的积极方面,包括对象的共享状态,知道结构中任何地方发生变化的可能性,以及在任何一点制作私人副本。
这是一个实验。我们不确定最终结果是否有用,但至少我们可以使用一些不错的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
是一个类,一个didSet
的people
不叫,因为价值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
}
如果我们真的改变它的价值didSet
,people
那么现在会被召唤。但是我们将第一个人的副本而不是引用传递给详细视图控制器,因此详细视图控制器正在更新其自己的副本Person
。people
只有在我们直接在数组中进行更改时才会调用观察者:
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"
我们的下标实施是错误的。我们正在捕获初始值,但我们应该捕获对该值的引用。我们会做一招,隐藏value
和observer
的Var
其初始内部:
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
我们回到我们原来的代码和应用Var
中PersonViewController
。这样,视图控制器可以更新其模型,并将这些更改反映回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
。添加该功能将是一项工作