Swift 值类型使用Copy-On-Write
什么是Copy-On-Write(写时复制)?
我们将一个值类型分配给另一个值类型时,我们都有原始对象的副本:
let myString = "Hello"
var myString2 = myString // myString is copied to myString2
myString2.append(" World!")
print("\(myString) - \(myString2)") // prints "Hello - Hello World!"
如果我们只复制一个普通的字符串,我们可能不会有性能问题。
当我们拥有包含数千个元素的数组时,我们可能会开始遇到一些麻烦,并且我们将它们复制到我们的应用程序中。为此,Array 有一种不同的复制方式,称为写时复制。
当我们将一个数组分配给另一个数组时,我们没有副本。这两个数组共享同一个实例。这样,我们就没有一个大数组的两个不同副本,我们可以使用同一个实例,具有更好的性能。然后,当两个数组之一发生变化时,我们就有了一个副本。
举个栗子:
var array1 = [1, 2, 3, 4]
address(of: array1) // 0x60000006e420
var array2 = array1
address(of: array2) // 0x60000006e420
array1.append(2)
address(of: array1) // 0x6080000a88a0
address(of: array2) // 0x60000006e420
func address(of object: UnsafeRawPointer) -> String {
let addr = Int(bitPattern: object)
return String(format: "%p", addr)
}
我们可以注意到,在这个例子中,两个数组共享相同的地址,直到其中一个发生变化。这样,我们可以array1多次分配给其他变量,而不必每次都复制整个数组,而只是共享同一个实例,直到其中一个发生变化。这对我们应用程序的性能非常有用。
不幸的是,并不是所有的值类型都有这种行为。这意味着,如果我们有一个包含大量信息的结构体,我们可能需要一个写时复制来提高我们应用程序的性能并避免无用的副本。因此,我们必须手动创建此行为。
手动写时复制
让我们考虑一个User
我们想要使用写时复制的结构:
struct User {
var identifier = 1
}
我们必须开始创建一个具有通用属性的类T
,它包装了我们的值类型:
final class Ref<T> {
var value: T
init(value: T) {
self.value = value
}
}
我们使用class
——这是一种引用类型——因为当我们将一个引用类型分配给另一个引用类型时,两个变量将共享同一个实例,而不是像值类型那样复制它。
然后,我们可以创建一个struct
to wrap Ref
:
struct Box<T> {
private var ref: Ref<T>
init(value: T) {
ref = Ref(value: value)
}
var value: T {
get { return ref.value }
set {
guard isKnownUniquelyReferenced(&ref) else {
ref = Ref(value: newValue)
return
}
ref.value = newValue
}
}
}
因为struct
是值类型,所以当我们将它分配给另一个变量时,它的值被复制,而属性的实例ref
仍然由两个副本共享,因为它是一个引用类型。
然后,我们第一次更改value
两个Box
变量中的一个时,我们创建了一个新实例`ref
guard isKnownUniquelyReferenced(&ref) else {
ref = Ref(value: newValue)
return
}
通过这种方式,这两个Box
变量不再共享同一个ref
实例。
isKnownUniquelyReferenced返回一个布尔值,指示给定对象是否已知具有单个强引用。
这里是整个代码:
final class Ref<T> {
var value: T
init(value: T) {
self.value = value
}
}
struct Box<T> {
private var ref: Ref<T>
init(value: T) {
ref = Ref(value: value)
}
var value: T {
get { return ref.value }
set {
guard isKnownUniquelyReferenced(&ref) else {
ref = Ref(value: newValue)
return
}
ref.value = newValue
}
}
}
let user = User()
let box = Box(value: user)
var box2 = box // box2 shares instance of box.ref
box2.value.identifier = 2 // Creates new object for box2.ref
结论
这种方法的另一种替代方法是使用Array(而不是Box
)来包装要在写入时复制的值类型。不幸的是,使用Array的方法有一些缺点。你可以在 Apple 的optimisation tips找到更多详细信息。