测试已测试 | ✓ |
语言语言 | SwiftSwift |
许可 | MIT |
发布最后发布 | 2017年10月 |
SwiftSwift 版本 | 4.0 |
SPM支持 SPM | ✓ |
由 Kevin Lundberg 维护。
CopyOnWrite
μ框架封装了 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
中的引用被重新分配,因此引用此属性的所有内容都必须也是可变的,这有助于保证你不会意外地更改你的引用类型中的值,如果你的结构体存储在let
常量中。
然而,调用正确的属性在正确的时间是你的责任。编译器或此类型无法阻止你这样做
func update(_ num: Int) {
storage.reference.num = num
}
虽然这段代码可以编译,但会导致原始问题重新出现,即在一个变量上调用update
会影响另一个变量中存储的值。
强烈建议将你的写时复制实例变量设置为私有的,这样你的API的外部客户端在尝试直接访问它时不会做错事。提供函数/属性以暴露你想要的功能,并支持引用对象。
此库还提供了一个你可以选择的规范,以防你发现自己正在你的类型的许多地方重复相同的CopyOnWrite闭包。
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
的方法实际上会创建一个不可变版本,当你尝试调用其可变方法时,你的程序会崩溃。
将以下行添加到你的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文件。