对于 Sequence 协议来说,表达的是既可以是一个有限的集合,也可以是一个无限的集合,而它只需要提供集合中的元素和如何访问这些元素的接口即可。那对于 Collection 来说它是一个继承自 Sequence 的协议。
一、Sequence
1. for in 的本质
如图,Sequence 是通过 Iterator 来访问元素的。Iterator 是一个迭代器,那什么叫迭代器呢,我们来看一段代码,如下:
let nums = [1, 2, 3, 4, 5];
for element in nums {
print(element)
}
Swift 中的 for in 其实是一个语法糖,那么它的本质是什么呢,我们把它编译成 sil 的代码来看一下,如何编译成 sil 文件在《方法》中有详细的介绍。
我们直接看到 main 函数,看我们写的 for in 是如何遍历的,如图:
可以看到,当执行 for in 的时候,本质上会通过 Collection 创建一个迭代器,然后把当前的数组传给迭代器,最后调用迭代器的 next 方法将数组的元素遍历出来。
所以本质上,当我们使用 for in 的时候,其实都是使用这个集合的迭代器来遍历当前集合或者序列当中的元素。
2. 通过 Sequence 表达有限集合
接下来我们去了解一下这个迭代器:IteratorProtocol。
public protocol IteratorProtocol {
/// The type of element traversed by the iterator.
associatedtype Element
mutating func next() -> Self.Element?
}
IteratorProtocol 有一个关联类型,这个关联类型是迭代器遍历的元素类型。它还有一个 next 方法,可以通过调用 next 方法来返回元素。
Sequence 也是一个协议类型,我们来看一下它的定义:
public protocol Sequence {
/// A type representing the sequence's elements.
associatedtype Element where Self.Element == Self.Iterator.Element
/// A type that provides the sequence's iteration interface and
/// encapsulates its iteration state.
associatedtype Iterator : IteratorProtocol
/// Returns an iterator over the elements of this sequence.
func makeIterator() -> Self.Iterator
// other function
// ......
}
接下来我们通过 Sequence 来表达一个有限的集合,首先自定义一个遵守 Sequence 的结构体 SHSequence:
struct SHSequence: Sequence {
typealias Element = Int
var arrCount: Int
func makeIterator() -> some IteratorProtocol {
<#code#>
}
}
指定这个关联类型为 Int 类型,定义一个 arrCount 属性,然后实现 makeIterator,需要注意的是这个 makeIterator 的返回值,它要求返回一个 IteratorProtocol 的抽象类型,也就是一个迭代器。
自定义的迭代器如下:
struct SHIterator: IteratorProtocol {
typealias Element = Int
let seq: SHSequence
init(_ seq: SHSequence) {
self.seq = seq
}
var count = 0
mutating func next() -> Int? {
guard count < seq.arrCount else {
return nil
}
count += 1
return count
}
}
这个迭代器接收一个 SHSequence 类型,并且通过 next 方法将元素返回。那这个 next 方法就随便写了,主要是测试一下能不能通过 next 返回元素。
这个时候 makeIterator 方法的实现如下:
func makeIterator() -> SHIterator {
return SHIterator(self)
}
测试代码如下:
let seq = SHSequence(arrCount: 10)
for element in seq {
print(element)
}
打印结果:
1
2
3
4
5
6
7
8
9
10
二、Collection
Collection 是一个序列,其元素可以被多次遍历。通过定义 startIndex 和 endIndex 属性,表示集合起始和结束位置。
1. 环形数组
接下来我们通过 Collection 来表达一个环形数组,什么是环形数组呢?对于普通数组来说,头尾的下标都是固定的,头下标为 0,尾下标为 length - 1。而环形数组(circular array)通常使用两个额外的数字去标识数组的头下标和尾下标。
接下来我们定义一个结构体 RingBuffer:
struct RingBuffer<Element>{
internal var _buffer: ContiguousArray<Element?>
internal var headIndex: Int = 0
internal var tailIndex: Int = 0
internal var mask: Int{
return self._buffer.count - 1
}
init(initalCapacity: Int) {
let capcatiy = initalCapacity.nextPowerOf2()
self._buffer = ContiguousArray<Element?>.init(repeating: nil, count:capcatiy)
}
mutating func advancedTailIndex(by: Int){
self.tailIndex = self.indexAdvanced(index: self.tailIndex, by: by)
}
mutating func advancedHeadIndex(by: Int){
self.headIndex = self.indexAdvanced(index: self.headIndex, by: by)
}
func indexAdvanced(index: Int, by: Int) -> Int{
return (index + by) & self.mask
}
mutating func append(_ value: Element){
_buffer[self.tailIndex] = value
self.advancedTailIndex(by: 1)
if self.tailIndex == self.headIndex {
fatalError("out of bounds")
}
}
mutating func read() -> Element?{
let element = _buffer[self.headIndex]
self.advancedHeadIndex(by: 1)
return element
}
}
这个结构体通过一个 ContiguousArray 类型的 _buffer 来记录环形数组的元素,ContiguousArray 可以理解为存粹的 Swift 的数组。和 OC 没有任何关系的数组,它的效率比 Array 更快。
接下来我们通过 headIndex 和 tailIndex 来表示环形数组的起始和结束的位置。剩下的都是这个环形数组的一些初始化方法,移动环形数组的起始和结束的位置的方法,以及追加元素和读取元素的方法和获取元素索引的方法。
我们看到初始化方法,nextPowerOf2 这个方法表示 2^n,这个表示使 capacity 始终为 2^n,其实现如下:
extension FixedWidthInteger {
/// Returns the next power of two.
@inlinable
func nextPowerOf2() -> Self {
guard self != 0 else {
return 1
}
return 1 << (Self.bitWidth - (self - 1).leadingZeroBitCount)
}
}
2. MutableCollection
MutableCollection 允许集合通过下标修改自身元素。根据这一点我们对 RingBuffer 进行一个 extension,代码如下:
extension RingBuffer: Collection, MutableCollection {
var startIndex: Int{
return self.headIndex
}
var endIndex: Int{
return self.tailIndex
}
subscript(position: Int) -> Element? {
get{
return self._buffer[position]
}
set{
self._buffer[position] = newValue
}
}
func index(after i: Int) -> Int {
return (i + 1) & self.mask
}
}
在这里 RingBuffer 遵循 Collection 协议,此时我们就需要实现 startIndex、endIndex 和 index(after:) 方法,index(after:) 是为了便于移动当前索引的位置。
遵守 MutableCollection 协议是因为为了实现下标的 setter 方法,便于在语法上直接通过下标来访问并修改这个元素的值,例如数组:
var nums = [1, 2, 3, 4]
// 直接通过下标来修改元素的值
nums[2] = 8
3. RangeReplaceableCollection
RangeReplaceableCollection 允许集合修改任意区间的元素。此时我们对 RingBuffer 进行一个 extension。
extension RingBuffer: RangeReplaceableCollection {
init() {
self.init(initalCapacity: 10)
}
func remove(at position: Int) -> Element? {
// ......
return nil
}
}
RangeReplaceableCollection 协议要求我们实现一个 init 方法,所以在这里就实现了一个默认的 init。接下来实现一个 remove(at:) 方法,对于环形数组来说,当我们删除一个元素的时候,首先把这个元素剔除,然后后面的元素往前移一个位置,然后再把尾节点指向当前空闲节点的位置。如果我们删除的位置,刚好是 headIndex 的位置,后面的元素也要往前移一个位置。
那 remove(at:) 的实现如下:
mutating func remove(at position: Int) -> Element? {
let element = _buffer[position]
_buffer[position] = nil
switch position {
case headIndex:
advancedHeadIndex(by: 1)
default:
var currentIndex = position
var nextIndex = indexAdvanced(index: position, by: 1)
while nextIndex != tailIndex {
_buffer.swapAt(currentIndex, nextIndex)
currentIndex = nextIndex
nextIndex = indexAdvanced(index: currentIndex, by: 1)
}
advancedTailIndex(by: -1)
}
return element
}
4. BidirectionalCollection
BidirectionalCollection 可以向前或向后遍历集合。比如可以获取最后一个元素、反转序列、快速的获取倒序等等。
既然正序是通过 subscript 和 index(after:) 来实现的,那么倒序添加一个 index(before:) 就可以往前递归了,这就好像双向链表 一样,只不过双向链表获取的是值,而这里的集合获取的都是索引。
代码如下:
extension RingBuffer: BidirectionalCollection{
func index(before i: Int) -> Int {
return (i - 1) & self.mask
}
}
5. RandomAccessCollection
RandomAccessCollection 可以任意访问集合元素。代码如下:
extension RingBuffer: RandomAccessCollection{
func index(_ i: Int, offsetBy distance: Int) -> Int {
return (i + distance) & self.mask
}
func distance(from start: Int, to end: Int) -> Int {
return end - start
}
}
测试代码如下:
var buffer: RingBuffer = RingBuffer<Int>.init(initalCapacity: 10)
for i in 0..<10 {
buffer.append(i)
}
print(buffer.index(0, offsetBy: 2)) // 2
当然,对于这个集合我们不去实现也是可以的。因为我们把 index(after:) 和 index(before:) 已经实现了,对于系统来说,它是可以通过这两个方法来推断当前要移动的距离。但是考虑到效率的问题,在这里直接实现会比系统去推断要好得多,因为直接省去了系统推断的时间。