本篇是对《swift进阶》中第5章内容的笔记,主要内容介绍Swift中的一个特性:写时复制。
在Swift标准库中,比如Array、Dictionary和Set类的集合类型是通过一种写时复制的技术实现的。那么什么是写时复制呢?比如说我们在创建一个数组x的时候,将其赋值给另外一个变量的时候,就会发生复制,在内部,这些结构体含有指向某个内容的引用。而内存也就是数组中元素的所存储的位置。因为它们都存在堆上,所以这两个数组的引用会指向内存中的同一个位置,所以说这两个数组共享了它们的存储的部分。但是如果我们改变x的时候,共享内容就会被监测,内存将会被复制。这样,就会出现两个独立的变量。代码如下:
var x = [1,2,3]
var y = x
print(x) //[1,2,3]
print(y) //[1,2,3]
x.append(4)
print(x) //[1,2,3,4]
print(y) //[1,2,3]
如果 Array 结构体中的引用在数组被改变的一瞬间时是唯一的话 (比如,没有声明 y),那么也不会有复制发生,内存的改变将在原地进行。这种行为就是写时复制。当自己的类型内部含有一个或多个可变引用,同时还想要保持值语义,并且避免不必要的复制时,为类型实现写时复制是有意义的。所以下面就看下写时复制的方式:
var input: [UInt8] = [0x0b,0xad,0xf0,0x0d]
var other: [UInt8] = [0x0d]
var d = Data(bytes: input)
var e = d
d.append(contentsOf: other)
d // 5 bytes
e // 4 bytes”
//同样,如果我们将Data改为NSMutableData的话,代码则会出现下列情况
var f = NSMutableData(bytes: &input, length: input.count)
var g = f
f.append(&other, length: other.count)
print(g) //<0badf00d 0d>
print(f) //<0badf00d 0d>
从上面例子可以看出f和g都引用了同样的对象,指向了同一块的内容,所以其中的一个变化也就带动了另外的一个变化。从上面的例子中我们也能验证到上篇文章中所讲到的值类型
和引用类型
的差异。
如果我们需要复制结构体变量,那么进行的会是浅复制。这也就意味着对象本身不会被复制,只有对象的引用会被复制(如果对深复制、浅复制了解不多,可以百度查询)。
低效方式
要实现写时复制的话,我们需要将变量设置为结构体的私有属性,这样也就会再直接更改变量,而是会通过一个对象的计算属性来访问。那么这个计算属性就会复制变量并将其返回。代码如下:
struct MyData {
fileprivate var _data: NSMutableData
var _dataForWriting: NSMutableData {
mutating get {
_data = _data.mutableCopy() as! NSMutableData
return _data
}
}
init(_ data: NSData) {
self._data = data.mutableCopy() as! NSMutableData
}
}
extension MyData {
mutating func append(_ other: MyData) {
_dataForWriting.append(other._data as Data)
}
}
let theData = NSData(base64Encoded: "wAEP/w==", options: [])!
var x = MyData(theData)
let y = x
x._data===y._data // true
x.append(x)
x // <0badf00d 0d>
y // <0badf00d 0d>
x._data===y._data //false
通过上面代码可以看出如果_dataForWriting
更改结构体,时这个属性的getter就会被标记为mutating
,这也就代表了我们只能用var方式来生命变量的使用。因此结构体也就具有值语义。所以当代码中的x、y两个变量虽然会继续指向相同的NSMutableData
,但是当x发生变化时,则就会产生了复制。当然,这种方式看起来很好,不过要是多次更改同一个变量时,效率并不是那么得高。
高效方式
上面基本实现了写时复制,但是对一个变量多次更改时效率则会显得低下。所以为了提供高效的写时复制的特性,我们需要知道一个对象是否是唯一的。如果是唯一的引用,那么久可以直接原地修改对象,如果不是,就需要在修改前来创建对象的复制。
在 Swift 中,我们可以使用 isKnownUniquelyReferenced
函数来检查引用的唯一性。如果你将一个 Swift 类的实例传递给这个函数,并且没有其他变量强引用这个对象的话,函数将返回 true。如果还有其他的强引用,则返回 false。针对这点,我们就可以针对Objective-C
来做出一些改变,毕竟isKnownUniquelyReferenced
检查Objective-C
的类会直接返回false
全局变量
final class Box {
var unbox: A
init(_ value: A) { self.unbox = value }
}
var z = Box(NSMutableData())
isKnownUniquelyReferenced(&z) // true
var k = z
isKnownUniquelyReferenced(&z) // false
结构体
struct MyData {
fileprivate var _data: Box
var _dataForWriting: NSMutableData {
mutating get {
if !isKnownUniquelyReferenced(&_data) {//检查对_data的引用是否是唯一性
_data = Box(_data.unbox.mutableCopy() as! NSMutableData)
print("Making a copy")
}
return _data.unbox
}
}
init(_ data: NSData) {
self._data = Box(data.mutableCopy() as! NSMutableData)
}
}
extension MyData {
mutating func append(_ other: MyData) {
_dataForWriting.append(other._data.unbox as Data)
}
}
let someBytes = MyData(NSData(base64Encoded: "wAEP/w==", options: [])!)
var empty = MyData(NSData())
var emptyCopy = empty
for _ in 0..<5 {
empty.append(someBytes)
}
empty //
emptyCopy // <>
通过运行以上代码,调试语句中只在第一次被调用,在下面的循环着那滚,因为引用都是唯一的,所以就没有进行复制操作。
注意事项
当我们将结构体放入字典、结合以及自定义的类型时,则会出现一些问题。比如字典,字典的下表会在字典中寻找值,然后返回。所以返回的是找到的值的复制,这样结构体已经不会被唯一引用,所以会发生复制。同样的比如数组,在直接用下标访问的时候没问题的,但是如果加一个中间变量的话,则就会发生复制。代码如下:
final class Empty { }
struct COWStruct {
var ref = Empty()
mutating func change() -> String {
if isKnownUniquelyReferenced(&ref) {
return "No copy"
} else {
return "Copy"
}
}
}
var dict = ["key": COWStruct()]
dict["key"]?.change() // Optional("Copy")]
var array = [COWStruct()]
array[0].change() // No copy
var otherArray = [COWStruct()]
var x = array[0]
x.change() // Copy
所以当在使用自己定义结构体以及将一个写时复制的结构体放到字典中的话,如果要避免这种写时复制,可以将值用类封装起来,这将为值赋予引用语义。
总结
优点:写时复制会在创建值类型的自定义结构体的同时,保持像对象和指针那样的高效操作。如果使用结构体,就不要操心去手动复制那些结构体。结构体的实现已经处理好了。
当定义结构体和类的时候,需要特别注意那些原本就可以复制和可变的行为。结构体应该是具有值语义的。当在一个结构体中使用类时,需要保证它确实是不可变的。如果办不到这一点的话,就需要 额外的步骤,或者就干脆使用一个类,这样数据的使用者就不会期望它表现得像一个值。
Swift 标准库中的大部分数据结构是使用了写时复制的值类型。比如数组,字典,集合,字符串等这些类型都是结构体。当我们将数组传递给函数时,我们知道这个函数一定不会修改原来的数组,因为它所操作的只是数组的一个复制。同样地,通过数组的实现方式,我们也知道不会发生不必要的复制。
在当创建结构体时,类也还是有其用武之地的。有时候你会想要定义一个只有单个实例的从不会被改变的类型,或者你想要封装一个引用类型,而并不想要写时复制。还有时候你可能需要将接口暴露给 Objective-C,这种情况下我们也是无法使用结构体的。通过为你的类定义一个带有限制的使用接口,还是能够做到让其不可变的。
当在使用自己定义结构体以及将一个写时复制的结构体放到字典中的话,如果要避免这种写时复制,可以将值用类封装起来,这将为值赋予引用语义。
Array 通过使用地址器 (addressors) 的方式实现下标。地址器允许对内存进行直接访问。数组的下标并不是返回元素,而是返回一个元素的地址器。这样一来,元素的内存可以被原地改变,而不需要再进行不必要的复制。