集合(Collection)是建立在序列(sequence)上层的类型,它添加了可重复遍历元素和根据下标访问元素的功能。
为了具体说明Swift中的集合实现原理。我们会实现一个自己的集合。可能Swift标准库中没有实现的最有用的容器类型就是队列(queue)了。Swift的数组可以很容易的拿来当栈使用——append就是压栈,removeLast就是弹栈。但把数组当队列用就不合适了。你可以把append和removeAtIndex(0)方法结合起来用,但移除队列中的元素(不是最后一个)是一个O(n)的操作。因为数组元素在内存中连续存储,移除一个元素意味着后面的元素都要向前移动来弥补被移走的元素留下的空缺。
在我们实现队列之前,我们最好先确定一下我们的队列需要什么功能。一个比较好的方法是先定义一个描述队列的协议:
protol QueueType { // 自己持有的元素类型 associatedtype Element mutating func enqueue(element: Element) mutating func dequeue() -> Element? }尽管这是一个非常简单的队列协议的定义,他依然提供了很多队列的定义。比如,队列被定义为一个范性容器,通过类型别名Element,他可以容纳任何类型的元素,它对元素类型没有要求,只要保证是某一个具体的类型即可。
需要强调一点对于enqueue和dequeue方法的时间复杂度没有明确要求。我们固然可以规定这两个方法在O(1)的常量时间内完成,而且这样会给用户留下高性能的印象,但同时也会不再适合某些数据结构。比如优先队列(priority queue)的入队方法需要O(log n)的时间复杂度。
这个协议没有提供peek方法允许用户在获取队首元素的同时不用dequeue。这意味着实现这个协议的队列不具备这一特性(比如操作系统和网络的队列接口就没有peek方法)。它也没有这两个操作是否是线程安全的,以及队列是否是一个集合类型(尽管我们将要实现的版本确实如此)。
这个协议甚至都没有指定队列是否是先入先出,也就是说可能是后入先出的。如果把apend当做入队,isEmpty配合removeLast实现队列功能,我们还可以让数组也实现这个协议
不过这个协议还是指定了一些信息的。比如与removeLast不同的是,dequeue函数返回的是可选类型的值。如果队列为空,dequeue会返回nil,如果数组为空,调用removeLast会导致程序退出。
这种风格很大程度上基于个人偏好。Swift的数组更倾向于在调用方法前由用户考虑数组当前的状态,再决定不能调用方法。最明显的一个例子是数组的下标,你最好确保数组有不少于十个元素,然后才去获取下标为9的数组元素。调用空数组的removeLast方法会导致程序退出。
Swift这么做主要考虑到数组切片的使用。在Swift中,计算下标其实是很罕见的:
遍历结合元素可以用: for x in collection遍历集合切片可以用: for idx in collection.indices获得所有元素和下标: for (num, element) in collection.enumerate()找到某个指定的元素: if let idx = collection.indexOf({ someMatchingLogin($0) })变换集合中的所有元素: array.map {someTransformation($0)}获取符合某个要求的元素: collection.filter{someCriteria($0)}一旦你发现你用到了形如:
for i in collection.startIndex...collection.endIndex { // 某些操作 }赶紧挺犀利想想你是否真的需要这样写。有时候你确实需要这么做,但是大多数时候你可以用一种更清楚易读的方法来实现同样的功能。手动管理数组下标在我看来极容易导致bug,所以最好不要这么做。如果必须这么做的话,我们可以用一些可以重用的范型函数来代替这种写法。这样就可以把经过精心测试的某一小段用于计算下标的代码封装起来。
有时候非用下标不可,那么久需要极其小心处理下标计算背后的逻辑。所以解封通过下标脚本获取的值,往往是多此一举的——这意味着你其实不相信你的代码。但有时候正是因为你过于相信自己的代码,所以有可能直接强制解封(force-unwrapping)获取到的值,这么做既让人反感,同时也不是什么好习惯。常在河边走哪有不湿鞋。如果你的代码到处都是强制解包,迟到要掉进坑里。所以不要让这种不好的风格成为习惯。
removeLast函数要稍微复杂些。如果把数组当做栈来用,只要数组不是空的,你就可能希望数组最后一个元素出栈:
while !array.isEmpty { // 必须用!.,isEmpty来保证数组不为空 let top = array.removeLast() // 处理top元素 }但是如果让dequeue函数的范围值也是可选类型,你就可以把代码缩短到一行,同时也不会出什么错误:
while let x = q.dequeue() { // 处理队列元素 }这么做(为了获取简洁性和安全性而在明知集合不为空时候还要进行一层封装)是否利大于弊,就取决于你自己了。
现在我们定义好了队列,是时候实现它了。
下面是一个非常简单的队列代码,只包含了enqueue和dequeue函数的实现。
因为我们已经把队列的范型占位符定义为Element了,也就是实际需要的类型别名,所以就没有必要再定义类型别名了。因为占位符只是你随便取的一个名字,如果我们让他叫Foo,我们可以再加一句typealias Element = Foo 或者干脆就让Swift的类型推导功能根据enqueue和dequeue的返回值类型自己推理。
/// 一个高效的FIFO队列,元素类型是Element struct Queue<Element>: QueueType { private var left: [Element] private var right: [Element] init() { left = [] right = [] } // 以O(1)的时间复杂度添加元素到队列的末尾 mutating func enqueue(element: Element) { right.apend(element) } // 以平均O(1)的时间复杂度移除队首的元素 // 如果队列为空就返回nil mutating func enqueue(element: Element) { guard !(left.isEmpty && right.isEmpty) else { return nil } if left.isEmpty { left = right.reverse() right.removeAll(keepCapacity: true) } return left.removeLast() } }这个实现其实是用两个栈(也就是Swift中的数组)来模拟队列。当元素入队时,他们进入右侧的栈。当元素出队时候,从左侧的栈出来。当左侧的栈为空时候,就颠倒右侧栈中所有元素并放入左侧栈中。
这就是为什么单个操作时间不定,但多次操作的平均时间是常量。通过同样的分析,我们也可以知道为什么向数组添加元素,也是花费常数级时间。当数组容量用尽时,系统会分配一块更大的空间然后把现有元素都拷贝过去。因为每次充分配空间都会将之前的容量翻倍,所以你可以套用同样的分析:“每次添加元素拿到一个令牌,扩容复制时花费一个令牌,花费总不会超过收入”。
现在我们有一个可以调用enqueue和dequeue方法的容器,为了让他真正成为一个集合,队列需要实现CollectionType协议:
extension Queue: CollectionType { var startIndex: Int { return 0 } var endIndex: Int { return left.count + right.count } subscript(idx: Int) -> Element { guard idx < endIndex else { fatalError("Index out of bounds") } if idx < left.endIndex { return left[left.count - idx.successor()] } else { return right[idx - left.count] } } }CollctionType协议定义了一个Index别名,不过和Element一样可以用方法和属性定义推导出来。
通过这几行代码,队列实现了CollectionType(自然也就实现了SequenceType协议)。队列现在就有超过40种方法和属性由那俩个协议进行管理了。下面给出一段队列的使用:
var q = Queue<String>() for x in ["1", "2", "foo", "3"] { q.enqueue(x) } //现在可以用for循环获取队列中元素了 for s in q { print(s) } //打印出 1 2 foo 3 //把队列传到那些能够接收它作为参数的方法中 q.joinWithSeparator(",") //返回的是字符串"1,2,foo,3" let a = Array(q) //数组a = ["1", "2", "foo", "3"] //调用SequenceType的拓展方法 q.map{ $0.uppercaseString } //["1", "2", "FOO", "3"] q.flatMap{ Int($0) } // [1,2,3] q.filter{ $0.characters.count > 1 } //调用Collection的拓展方法 q.isEmpty // false q.count // 4 q.first // "1" q.last // "3" while let x = q.dequeue() { // 处理队列元素 }我们模仿了数组的习惯,对于无效的下标不会返回可选类型的值而是直接报错。这里调用Queue.first返回了一个即将出队的元素,它的作用类似于之前所说的peek
不过先别急着高兴,SequenceType协议要求我们提供一个generate方法,来创建生成器,我们的代码中没有这么做,所以它能够通过编译么?
在Swift2.0以前,我们确实需要实现generate()方法——通过使用IndexingGenerator辅助结构体来创建一个生成器。这个生成器可以通过下标脚本从头到尾遍历元素:
extension Queue: CollectionType { // startIndex、endIndex和subscript的定义和之前一样 func generate() -> IndexingGenerator<Queue<Element>> { return IndexingGenerator(self) } }不过我们可以看到这个函数并没有什么实质性的内容,它只是一些固定的模板代码而已。所以在Swift2.0中,由于协议拓展的出现,CollectionType协议通过拓展,定义了一个默认的实现。这样我们就不用自己写这段代码了
我们在构造自己的队列时,有必要也让它实现一下ArrayLiteralConvertible协议。这允许用户通过熟悉的[value1, value2, …]语法来创建队列。这很容易实现:
extension Queue: ArrayLiteralConvertible { init(arrayLiteral elements: Element...) { self.left = elements.reverse() self.right = [] } }考虑到队列的逻辑,我们希望反转所有元素并放到左侧栈中,因为它们以后出队时也得放到左侧栈中。所以不如直接放在左侧栈,省去一次元素复制的操作。
现在我们可以很容易的用字面量创建队列了:
let q: Queue = [1,2,3]需要强调一点的是,Swift中的字面量和类型是不一样的。这里的[1, 2, 3]不是数组,而是数组字面量。它可以用于创建任何一个实现了ArrayLiteralConvertible协议的类型的实例对象。这个数组字面量内部含有整数字面量,可以用于创建任何实现了IntegerLiteralConvertible协议的类型。
这些字面量是有默认类型的,如果你没有指定它的类型,Swift会把它当做默认的类型。比如数组字面量会默认生成一个数组,整数字面量对应的默认类型是Int,字符串字面量对应的类型是String。但默认类型只在你不指明变量类型时候才会生效,比如之前我们把队列声明为整数的队列,但我们也可以把它定义为别的类型的整数:
let byteQueue: Queue<Int8> = [1, 2, 3]字面量的类型一般可以通过上下文推断出来。比如下面就是一个接受字面量作为参数的函数:
func takesSetOfFloats(floats: Set<Float>) { //... } //这样,字面量就会被当做Set<Float>而不是Array<Int> takesSetOfFloats([1, 2, 3])下一个需要实现的协议是RangeReplaceableCollectionType,这个协议有三个要求:
一个空的初始化方法。它通过一个函数创建新的空集合作为占位符,这些空集合都是相同的类型,这在范型编程中很有用。
一个空的初始化方法。它通过一个函数创建新的空集合作为占位符,这些空集合都是相同的类型,这在范型编程中很有用。
一个replaceRange函数。这个函数用一个新的集合替换原来集合中指定的范围。RangeReplaceableCollectionType协议充分展示了协议拓展的强大。你只要实现一个非常灵活的replaceRange方法,立刻就可以使用由此得到的一系列方法:
append和appendContentsOf方法——在末尾添加一个或多个新元素(相当于替换了endIndex..
extension Queue: RangeReplaceableCollectionType { mutating func reserveCapacity(n: Int) { return } mutating func replaceRange<C : CollectionType where C.Generator.Element == Element>(subRange: Range<Int>, with newElements: C) { right = right + left.reverse() left.removeAll(keepCapacity: true) right.replaceRange(subRange, with: newElements) } }你可以试着实现一个更高效的版本。可以判断一下subRange是否横跨了左右两个栈,还是只是在某一侧栈的内部。
这里我们没有必要实现init方法,因为它已经被实现了。在这段代码中,appendContentsOf和append方法把新的元素添加到队列末尾,这是符合队列自身逻辑的。