实现Swift的Array
当我们从Obj-C
转到Swift
时,会发现Swift
提供的数据结构大量使用值语义,如Array
、Dictionary
等,今天,我们将实现Array
的基本功能,来理解值语义以及写时复制。
值和引用语义
在实现之前,我们来简单讨论一下值语义和引用语义的区别。当我们使用Obj-C
及大多数其他面向对象语言开发时,经常会使用对象指针或引用,而这就是常说的引用语义,我们可以赋值一个对象实例的引用给一个变量:
MyClass *a = ...;
接着,可以将该变量赋值给另一个变量:
MyClass *b = a;
此时,a
和b
将指向同一个对象,如果指向的对象是可变的,当我们修改对象的内容时,两者对其都可见。
值语义相对比较简单,我们使用的如int
、struct
等都是值语义,声明为值语义的变量,其直接指向真正的值,而不是值的指针。所以当我们进行赋值时,将得到值的拷贝,如:
1 | int a = 42; |
此时,b
的值为43
,而a
依然为42
。
在Swift
中,class
类型仍然是引用语义,struct
则为值语义,所以,如果对class
类型进行赋值时,将得到一个指向同一个实例的引用,对实例对象内容的修改将对所有的引用可见;使用struct
时,则不会,每个变量之间都是独立的。
对于使用过Obj-C
开发过的人来说,转到Swift
使用其提供的数组和字典时,可能会不习惯,因为Swift
中两者已经变成了值语义,如:
1 | var a = [1, 2, 3] |
对于大多数语言,如上代码的结果一般都是a
和b
都指向同一个数组[1, 2, 3, 4]
。而在Swift
中,a
指向[1, 2, 3]
,b
则指向[1, 2, 3, 4]
。
实现值语义
如果对象包含固定数量的数据,那么实现就比较简单:我们直接将数据放到struct
中即可。比如想要一个2D Point
类型且满足值语义,可以创建一个struct
包含x``y
的值:
1 | struct Point { |
固定数量实现起来比较简单,但是我们使用的Array
,它的个数不定,我们不可能直接把所有数组元素放到struct
中,所以,我们需要创建一个指针,用来指向数组元素的内存:
1 | struct Array<T> { |
不过,有这还不够,我们还需要针对struct
的赋值和销毁做一些操作;当struct
被赋值时,需要对新的struct
的ptr
元素赋值,这时候需要将原struct
的ptr
指向的所有元素拷贝到一组新的内存块中,并将新的struct
的ptr
指向新的第一个元素的地址。当struct
销毁时,其指针指向的元素占用的内存同样需要被释放。而这两个需求,Swift
的struct
并没有提供自定义方法可以实现。
由于class
有deinit
方法,所以析构功能可以通过class
来实现,指针指向的内容可以在deinit
中进行处理,但是class
又不是值语义的,不过我们依然可以解决,既外封装struct
,使用struct
作为外部接口进行数组的使用,如下:
1 | class ArrayImpl<T> { |
接下来,在Array<T>
中添加一个接口,其内部则调用ArrayImpl
的实现。
如上,尽管使用了struct
,但是我们还并没有实现值语义,如果拷贝一个struct
,新的struct
依然指向相同的ArrayImpl
,而我们又没法自定义struct
的赋值操作,所以也没法拷贝。不过,我们可以从另一个角度去解决这个问题,我们可以思考一下,真正需要在什么时候进行拷贝的操作呢,答案就是做修改操作时,俗称的COW
,既只有当进行修改操作时,才进行真正的拷贝。
比如,实现append
方法来对ArrayImpl
的拷贝添加一个元素(假设ArrayImpl
已经实现了copy
方法):
1 | mutating func append(value: T) { |
这样,就实现了Array
的值语义,尽管a``b
赋值后依然指向相同的数据,但是只要修改其中的一个,就会进行拷贝操作,以保持数据的独立性。
如上的append
方法,虽然实现了功能,但是效率太差,如:
1 | var a: [Int] = [] |
在每一次进行迭代时,指向的数据都会进行拷贝的操作,然后立即销毁之前的存储内存,那么我们如何处理这种情况呢?
isKnownUniquelyReferenced
该API
返回一个布尔值,表明某个对象是否只有一个单独的强引用指向它。
有了这个API
,我们就可以修改一下append
方法,只在必要的时候进行拷贝:
1 | mutating func append(_ value: T) { |
ArrayImpl
接下来,我们将开始实现ArrayImpl
。数组包含3个属性:数据指针、数组中元素的个数,以及当前分配的内存可存放的总个数。数据指针和数组元素个数这两个值是必须的,不过我们应当预先分配一定量的空闲内存区域,以避免过多的reallocation
操作。
1 | class ArrayImpl<T> { |
接下来,实现init
方法,init
方法获取两个参数,count
、ptr
,拷贝指针的内容到新的对象。
1 | init(count: Int = 0, ptr: UnsafeMutablePointer<T>? = nil) { |
接下来,实现append
方法,首先检查是否需要重新分配内存,如果没有多余的空间,则需要一块新的内存:
1 | func append(obj: T) { |
首次分配内存时,分配容量初始为space
的2倍,且最小容量设置为16.
1 | let newSpace = max(space * 2, 16) |
接着将数据从原位置拷贝到新的指针所在的内存。
1 | newPtr.moveInitialize(from: ptr, count: count) |
拷贝完成之后,就可以释放原来的内存了:
1 | ptr.deallocate(capacity: count) |
现在,已经有足够的空间来存放新的元素,我们将其拷贝到当前所有元素中最后一个元素后面内存区域并将count
自增1:
1 | (ptr + count).initialize(to: obj) |
remove
操作也很简单,因为不需要重新分配内存。首先,清理待移除元素所在的内存:
1 | func remove(at index: Int) { |
moveInitialize
方法可以使所有剩余的元素移动到指定的内存区域:
1 | (ptr + index).moveInitialize(from: ptr + index + 1, count: count - index - 1) |
然后将count
减一来表明已经移除一个元素:
1 | count -= 1 |
当然,我们也需要实现copy
方法,以实现必要的拷贝操作(真正执行拷贝操作是在init
方法中):
1 | func copy() -> ArrayImpl<T> { |
最后,别忘了释放内存,我们可以在deinit
中进行内存的释放操作:
1 | deinit { |
这样,ArrayImpl
基本方法已经完成,接下来就是实现Array
的struct
的接口方法,其主要是调用ArrayImpl
的实现。
Array
接下来,实现Array
的一些接口方法,由于比较简单,直接上代码,代码如下:
1 | private mutating func ensureUnique() { |
最后,我们还要加上两个功能,下标访问以及for...in
迭代,下标可以通过实现subscript(index: Int)
方法,for...in
迭代可以通过实现Collection
协议的方法,具体如下:
1 | var count: Int { |
源代码及测试代码
所有实现已上传Github
:https://gist.github.com/zhongwuzw/2b194c9fb2e4aaaca04cedb79bf207f1
参考
https://www.mikeash.com/pyblog/friday-qa-2015-04-17-lets-build-swiftarray.html