封装 CopyOnWrite
类型的微框架,让实现值语义变得容易!
CopyOnWrite 是一个微框架,它为在包含引用类型的结构体中实现值语义提供了一个抽象。
值语义是一个概念,它适用于这样一种类型:你不能通过改变另一个变量来影响一个变量的值。任何改变某个变量状态的尝试都将保持在该变量的局部范围内。例如,String
类型是一种具有值语义的值类型(结构体)。
var s1 = "Foo"
var s2 = s1
s1.append("Bar")
print(s1) // FooBar
print(s2) // Foo
将 s1
赋值给 s2
的操作实际上创建了 s1
内容的副本,并将该副本存储到 s2
中,从而使对其中一个的更改与另一个隔离,这是定义类型是否具有值语义的关键细节。
然而,仅仅因为 String
是一个结构体并不意味着它能免费获得值语义。如果你的结构体类型包含内部引用,就像 String
一样,你必须有意识地、有目的地实现这种行为。考虑以下示例:
class Foo {
var num: Int = 0
}
struct Bar {
private var storage = Foo()
var num: Int {
return storage.num
}
func update(_ num: Int) {
storage.num = num
}
}
var bar1 = Bar()
var bar2 = bar1
bar1.update(100)
print(bar1.num) // 100
print(bar2.num) // 100
由于 Bar
结构体包含引用类型,因此在将 bar1 赋值给 bar2 时,引用会被复制,但内存中的实例保持不变,因此更改该实例的属性将影响对该实例的所有引用,这对于使用你的类型的人来说可能是意想不到的行为。有关更深入的详细信息,请阅读这篇文章并查看它引用的资源。
你可以使用这个 CopyOnWrite
库轻松实现值语义,以便在结构体中更改引用类型时,这些更改将保持在存储该结构体的特定变量的局部范围内,而不会影响结构体可能已存储的其他位置。
以下列方式修改前面的示例将使 Bar
类型具有值语义。
class Foo {
var num: Int = 0
}
struct Bar {
private var storage = CopyOnWrite(reference: Foo(), copier: {
let new = Foo()
new.num = $0.num
return new
})
var num: Int {
return storage.reference.num
}
mutating func update(_ num: Int) {
storage.mutatingReference.num = num
}
}
var bar1 = Bar()
var bar2 = bar1
bar1.update(100)
print(bar1.num) // 100
print(bar2.num) // 0
CopyOnWrite
的主要初始化器接受两个参数:要包装的对象,以及一个复制器闭包,该闭包仅在确定需要复制以保留值语义时才被调用。如果只有一个引用指向 CopyOnWrite
包装的内部引用,则不需要调用闭包,它将直接更新引用作为优化。一旦有多个值引用内部存储,CopyOnWrite
将检测到这一事实,并在更改引用之前运行复制闭包。
var bar3 = Bar()
bar3.update(42) // only one reference, no copy made
var bar4 = bar3
bar3.update(1024) // two references to Bar's internal storage in bar3 and bar4, so run the copy closure before making the change
bar3.update(0) // bar3 has a unique reference now after the previous copy, so it will not copy again
一旦你初始化了写时复制值,你就可以通过两个属性访问内部引用:
reference
- 适用于不可变操作:任何不会改变你存储的引用类型可观察状态的属性或方法都可以通过此属性安全调用。mutatingReference
- 适用于可变操作。任何会改变引用可观察状态的操作都必须通过此属性调用。正如你在上面的示例中看到的,Bar
上的 update
方法被更改为 mutating
。这是因为简单地访问 mutatingReference
可能会导致 CopyOnWrite
中的引用被重新赋值,因此任何引用此属性的内容也必须标记为 mutating,这有助于保证如果你的结构体存储在 let
常量中,你不会意外更改引用类型中的值。
然而,在正确的时间调用正确的属性是你的责任。编译器或此类型都无法阻止你这样做。
func update(_ num: Int) {
storage.reference.num = num
}
虽然这段代码可以编译,但它会导致最初的问题再次出现,即在一个变量上调用 update
会影响存储在另一个变量中的值。
强烈建议你的写时复制实例变量是私有的,这样你的 API 的外部客户端在尝试直接访问它时就不会做错事。提供函数/属性来暴露你想要的功能,并以引用的对象作为支持。
该库还提供了一个协议,你可以选择遵循该协议,以防你发现自己在类型中的许多地方重复相同的复制闭包。
public protocol Cloneable: class {
func clone() -> Self
}
如果你的类型可以遵循该协议,你可以像这样简单地将引用提供给 CopyOnWrite
。
extension Foo: Cloneable {
func clone() -> Self {
let new = self.init()
new.num = num
return new
}
}
struct Bar {
private var storage = CopyOnWrite(reference: Foo())
// ...
}
对于可能遵循 NSCopying
或 NSMutableCopying
的类型,也有针对这些类型的便利初始化器。
CopyOnWrite(copyingReference: MyNSCopyingType())
CopyOnWrite(mutableCopyingReference: MyNSMutableCopyingType())
注意为你要存储的正确类型使用正确的初始化器。如果你为像 NSMutableString
这样的类型使用 NSCopying
版本,则调用的 copy
方法实际上会创建一个不可变版本,当你尝试在其上调用可变方法时,你的程序将会崩溃。
CopyOnWrite
可通过 CocoaPods 获得。要安装它,只需将以下行添加到你的 Podfile 中:
pod 'CopyOnWrite', '~> 1.0.0'
CopyOnWrite
可以与 Carthage 集成。将以下内容添加到你的 Cartfile
以使用它:
github "klundberg/CopyOnWrite" ~> 1.0.0
将以下行添加到你的 Package.swift
文件中的依赖项列表中:
.package(url: "https://github.com/klundberg/CopyOnWrite.git", from: "1.0.0"),
只需将 CopyOnWrite.swift
文件复制到你的项目中。
Kevin Lundberg, kevin at klundberg dot com
如果你有任何想要看到的更改,请随时打开 pull request。请为任何更改包含单元测试。
CopyOnWrite 在 MIT 许可证下可用。有关更多信息,请参阅 LICENSE 文件。