Soroush khanlou sequence collection header

关于序列和集合需要知道的二三事

概述

我是 Soroush。在这里,我将向大家讲述关于**序列 (Sequence) 和集合 (Collection) **需要知道的二三事。

当我们在使用 Swift 的时候,我们可能需要一个有序的元素列表。绝大多数情况下,我们会选择使用数组。但是 Swift 中的数组(包括集合对象以及其他遵循 Collection 协议的对象)的建立都是经过深思熟虑的,其通过各种各样的协议架构、关联类型以及其它相关的组件组成了我们所使用的功能,并且我们每天都会去使用这些功能。在这里,我将探讨一下这些协议,以及我们该如何利用这些协议来构建我们所想要的功能。

关于序列和集合需要知道的二三事

所有的这些功能都是建立在一个名为 Sequence 的协议之上的。我们在使用数组时所用到的许多功能都是由Sequence 协议提供支柱的。例如,当您在使用 map 或者 filter 功能的时候,或者您需要找到序列中的第一个元素以便进行某些测试的时候,这些功能都是在这个名为 Sequence 的协议中所定义的。这个协议相当简单,可以说序列的一切功能都是建立在 Sequence 协议之上的。其余的协议就如同千层饼一样,一层一层地堆叠在一起。我将通过一层层揭示千层饼的一部分来完成这次讲演。之后,我们将讨论集合与双向集合 (Bidirectional Collection)。(不过关于随机访问集合 (Random Access Collection)、范围可替换集合 (Range Replaceable Collection) 以及可变集合 (Mutable Collection) 方面的内容我将不会涉及。

序列

我们首先从最基础的部分开始:序列(很简单粗暴)。序列是一个记录了一组元素的列表。它有两个重要的特性:第一,它的容量可以有限也可无限;第二,只可以迭代 (iterate) 一次。有些时候您或许可以多次对其进行迭代,但是我们不能保证您每次能够多次对其进行迭代。

protocol Sequence {
    associatedtype Iterator: IteratorProtocol
    func makeIterator() -> Iterator
}

序列协议有两个组成部分。一个是一个关联类型 (associated type),也就是这个 Iterator(迭代器)。这个关联类型必须要遵循 IteratorProtocol 协议;另一个组成部分是一个函数,它能够为我们构建 Iterator,这个函数返回的类型必须与我们声明的 Iterator 类型相同。

IteratorProtocol

对于 IteratorProtocol 而言,我们需要深入了解一下。这个协议看起来与 Sequence 协议类似。它有一个关联类型 Element,这个 Element 就是迭代器所声明的类型,或者说是需要迭代的类型;此外它还有一个名为 next 的函数,它会返回序列的下一个元素并对迭代器本身进行修改 (mutate)。

protocol IteratorProtcol {
    associatedtype Element
    mutating func next() -> Element?
}

Sequence 协议是基于 IteratorProtocol 构建的,而 Sequence 协议又为我们所需的功能提供了支撑。我们将以链表 (LinkedList) 为例。链表这个例子非常适合,因为它与 Sequence 贴合得很紧密:我们需要查看链表中一个元素,然后不停地查看下一个元素。

示例:链表

这里是链表的一个示例(如果您对链表的概念不熟悉的话,那么我来解释一下),我们获取到的首先是头元素 (first element),然后这个头元素会指向第二个元素,以此类推,直到最后一个元素为止。要在 Swift 中定义一个序列,或者说链表的话,有好几种方法可以实现,不过我更喜欢的方法是使用枚举。枚举可以使用泛型<T>,这也是我们的链表将要保存的类型。还有另外一件需要说明的事:就是这个 indirect 关键字。indirect 意味着我们需要在类型内部使用这个 LinkedList 结点类型(如果我这样做的话请不要感到惊愕。请让我任性一回!)。

Receive news and updates from Realm straight to your inbox

当我们在使用这个链表的时候,有这样两种情况。第一种情况是这个列表只有一个值,这个值的类型为 T。第二种情况时您确定链表拥有多个值,这样您可以去获取所关联的下一个值,这个值的类型与整个链表的类型完全相同。获取的元素将始终指向下一个元素,然后当您最终浏览完所有元素的时候,就意味着链表已经结束了。

但是这个链表的功能还有所缺失:我们不能对其进行枚举,我们不能使用循环(例如 for 循环),我们也无法使用 mapfilter 等功能。我们希望我们的链表能够遵循 Sequence 协议,这样我们便可以使用这些功能了。

为了实现这一点,我们必须去遵循 Sequence 协议,我们就必须要添加这个 makeIterator 函数,但是我们现在并不知道我们所需的 Iterator 类型是什么。您会注意到这里我们并没有添加 Iterator 这个关联类型。如果我们在这里添加了具体的类型,Swift 就会知道它应该使用哪种类型。我们知道我们需要某种类型的对象来执行迭代操作。

我们需要构建链表的迭代器。链表迭代器的类型是泛型 <T>,这意味着每次您调用 next 方法的时候都会返回一个值,而这个值的类型就是这个 T 类型——您可以向链表中放入任何您想要的类型。我们的迭代器也需要遵循 IteratorProtocol,我们可以对序列类型做出一些限制使其能够遵循这个协议。由于迭代器会逐步遍历序列,因此它的行为和指针很类似,它必须拥有一条状态,从而知道它位于序列的何种位置。这个状态将由 current 这个变量表示,它将指向链表的当前结点。这个结点同样也是泛型 T,和迭代器一样。

然后我们就可以开始实现我们想要的功能了,这将返回一个 T? 类型。除非返回的是 nil,否则就可以一直返回下一个值。一旦返回了 nil,这就表示我们处在序列的结尾,就不再返回值了。如果再继续访问,那么就应该一直返回 nil。因为我们使用的是枚举,所以我们可以做很多事。我们需要将其展开,然后看看里面的内容是什么。

为此,我们需要使用 switch 语句。我们对当前的链表节点进行 switch 操作,这时候我们会有两种可能的情况。第一种情况很简单:如果我们位于链表的末尾,那么我们需要返回 nil第二种情况是:如果我们取到值的话,这就意味着我们获取到了一个元素,因此也就可以执行 next 操作。我们知道我们需要将这个元素返回(很简单不是么)。我们可以直接将这个元素返回,但是下一个值该如何处理呢?接下来我们就需要让我们的链表结点指向下一个元素。我们可以更新 current 这个变量,然后将其设置为与 next 相同。这样做会使得在使用 switch 语句时选择到 current 变量的时候去调用 nextcurrent 变量会被更新成下一个值,这个时候我们就可以返回新的值,或者返回 nil 以表示这个序列已经结束。这就是我们实现 Sequence 协议所需要做的工作。我们现在可以知道迭代器的类型是什么,也就是一个 LinkedList 类型的迭代器,因此我们可以将它放在那里,我们可以将其设置为从 LinkedList 头结点开始。self 就是 LinkedList 的头结点,这样它便可以从头开始使用了。这就是我们将这个类型实现 Sequence 协议所需做的所有事情

indirect enum LinkedListNode<T> {
    case value(element: T, next: LinkedListNode<T>
    case end
}
  
extension LinkedListNode: Sequence {
    func makeIterator() -> LinkedListIterator<T> {
        return LinkedListIterator(current: self)
    }
}

struct LinkedListIterator<T>: IteratorProtocol {

    var current: LinkedListNode<T>

    mutating func next() -> T? {
        switch current {
        case let .value(element, next):
            current = next
            return element
        case .end:
            return nil
        }
    }
}

由于我们已经实现了 Sequence 协议,现在我们便可以使用 for 循环来对其进行遍历了,此外还可以使用 mapfilter 等操作(以及 Sequence 协议所附带的其他功能……这些功能可是相当的多!)。

现在让我们来探讨一下迭代器。我们之前已经创建了一个 LinkedList 对象了,其中存放了三个元素。

let iterator = LinkedListIterator<String>(current: linkedList)
print(iterator.next()) // => “A”

当我们创建出这个迭代器之后,其便包含了一个指向链表第一个值的指针,也就是所谓的 current 变量。接下来,我们可以通过 current 来访问链表头。当我们调用 iterator.next() 之后,就会返回值 A。接下来,迭代器会将 current 的引用进行值更新,从而指向链表中的第二个元素。

let iterator = LinkedListIterator<String>(current: linkedList)
print(iterator.next())
print(iterator.next()) // => “B”

如果再次调用 next() 方法的话,这个时候迭代器将会返回值 B,然后再次移动指针,使其指向链表中的下一个元素。这段过程周而复始,直到指针指向最后一个元素为止,这个时候它将会返回 nil,这之后无论您调用多少次 next(),迭代器都只会返回 nil但是如果我们想要为所有序列添加某些功能的话,应该怎么做呢?

有些时候我们会得到一对对象,此时您可能想要知道有多少对象能够通过测试。这是一个很好的例子。我们将使用 filter 来从数组中获取能通过「测试」的所有元素,然后我们对过滤后的数组调用 count 方法。这看起来还不错,但是这个时候我们还是创建了一个多余的数组出来,然后通过这个数组来获取数目,随后再将这个数组抛弃。这并不是很理想。我更喜欢 users.count 的这种写法,然后再将测试传递进这个数组当中。这种写法更具表现力,并且如果您曾经写过 Ruby 的话,那么您肯定对可枚举模块 (Enumerable Module) 非常熟悉。这种写法要更好一些,这样我就可以将这个函数添加到我的所有序列当中。

首先,我们需要为 Sequence 协议建立一个扩展。我们知道这里我们需要添加一个函数,这个函数的名称为 count。这个函数将返回一个整数,此外我们也需要向其传递一个参数。我将这个参数命名为 shouldCount,因为这更容易理解和阅读。要注意的一件事情是:序列内部的元素可以使用 iterator.element 来引用。这样不管序列内部的元素类型是什么,都可以获取到里面是字符串还是 user 这个对象。通过序列,我们知道需要对一些东西进行迭代操作,而这正是我们通过实现 Sequence 协议所得到的能力。我们可以在循环之中对其进行访问。我们知道我们将对这些元素进行迭代,随后我们需要获取并持有每个元素。

此外我们也知道需要一个 count 操作。因此我们要持有一个 count 变量。它将会从 0 开始,然后在最后返回。循环中间的这部分操作有点取巧的感觉。如果我们允许对元素进行计数、测试通过并且 user 的类型为 admin 的话,那么我们就知道我们需要给 count 加 1了。是不是非常直接!

let numberOfAdmins = users.filter({ $0.isAdmin }).count // => fine

let numberOfAdmins = users.count({ $0.isAdmin }) // => grlet numberOfAdmins = users.count({ $0.isAdmin }) // => great

extension Sequence {

    func count(_ shouldCount: (Iterator.Element) -> Bool) -> Int {

        var count = 0

        for element in self {

           if shouldCount(element) {

               count += 1

           }

        }

        return count

   }

}

这就是我们该如何向所有序列添加扩展的方式。您可以像其他任何类型一样,对序列进行扩展,然后通过 iterator.element 来引用得到 Sequence 当中的类型。借助扩展,我们可以实现任何想要实现的操作;这是一个非常有用的扩展,我的几乎所有项目都添加了这个扩展,或许我觉得它也可以放到 Swift 标准库当中。

序列的另一个有用的功能扩展就是「分组成对 (each pair)」(这是我自己命名的称呼)。它可以让连续的元素互相成对,然后将它们组合在元组当中。假设您有一个数字序列,并且想要知道数字之间的差值的话,那么这个功能就非常有用。您可以通过将序列中的数字进行分组成对,然后两对之间进行互减。如果您有多个视图,然后想要在视图之间添加自动布局约束的话,那么这个功能也非常有用。比如说这个视图,然后这个视图,接着再来一个视图。您可以以结对 (pair) 的方式来处理这些视图,然后在视图之间添加约束,然后再去处理下一个结对。

zip(sequence, sequence.dropFirst()) // Sequence<(T, T)>

让我们来看一看如何借助标准库来实现这个功能。在标准库当中,我们将从序列开始。要获得分组成对的这个效果,我们需要创建该序列的一份拷贝,然后从这份拷贝中删除第一个元素。接下来,我们将使用 zip 操作将它们合并在一起,这会将相同的元素组合在一起,这个时候最后一个元素便会孤立出来。一旦我们得到了这些结对,那么剩余的操作就简单了。我们将这些元素配对在一起,下面就是见证奇迹的时刻。Boom shakalaka!现在我们边通过这个 zip 表达式将这两个序列分组成对在一起了。但是我想要让其成为 Sequence 的一个方法,这样我就可以进行链式调用了。这样我就可以执行 eachPair 然后再执行 mapfilter 等其他我想做的任何操作

extension Sequence 
  
  where Self.SubSequence: Sequence {

  Self.SubSequence.Iterator.Element == Self.Iterator.Element {
  
    func eachPair() -> AnySequence<(Iterator.Element, Iterator.Element)> {

      return AnySequence(zip(self, self.dropFirst()))
  
  }

}

我们可以从上面的方法开始。首先我们需要创建一个名为 eachPair 的函数,它将返回一个元组序列,其中元组包含了两个元素。这里面我们使用了 zip 和其他杂七杂八的东西。这里使用了一个额外的组件:AnySequence,这是一个类型擦除器 (type eraser)(我建议大家去看一看 Gwen Weston 关于类型擦除器的讲演)。问题是当我们尝试对其进行编译的时候,会发现它不起任何作用。Swift 编译器给我们的错误提示是:参数 Self.SubSequence 不符合预期的类型 Sequence。不必担心,序列具有另一个关联类型。这就是这个所谓的 SubSequence。由于 Swift 编译器的限制,您不能强制每个 SubSequence 都属于 Sequence。因此我们需要自行实现这一约束。

这里我们需要添加一条约束,表示“我知道这里将会出现一个 SubSequence,但是我保证这个 SubSequence 也属于 Sequence”。我们现在尝试对其进行编译,会遇到另一个错误:Cannot convert expression, return expression。这个提示很没意思,但是重要的是第二个部分的提示:Self.SubSequence.Iterator.ElementSelf.Iterator.Element 不匹配。也就是说,我们现在有了一个 SubSequence,并且我们知道 SubSequence 也是一个序列,但是我们不知道 SubSequence 内部的类型与我们序列内部的类型是否相同。因此我们必须为此再添加一条约束。

我们让 Self.SubSequence.Iterator.Element 等于 Self.Iterator.Element。这就可以让这段代码成功通过编译。现在我们就可以根据需要来使用 eachPair 了,不过处理这些关联类型的问题在使用 Swift 协议和集合系统当中常见的难点。好消息是,您还是可以获取到所需的结果。简而言之,这就是序列的全部内容了

集合

在介绍完序列之后,我们就再上一级,来谈论一下集合。每个集合都继承自 SequenceCollection 协议,从而修复了我们之前所说的关于序列的两个问题。所有集合都永远是有限的。这意味着您总能知道集合当中有多少个元素,它永远都不会是无限的,因此我们可以根据需要对集合进行多次迭代。而对于序列而言,我们只能够迭代一次,但是对于集合而言,我们可以一遍又一遍地进行迭代,这是一个非常棒的性质。

现在让我们来看一看 Collection 协议。

protocol Collection {
  associatedtype Index: Comparable
  var startIndex: Index
  var endIndex: Index
  subscript(position: Index) -> Iterator.Element { get }
  func index(after index: Index) -> Index
}

协议中新增的一个主要元素就是这个名为 Index 的新关联类型。如果您在使用数组的话,那么您可以将这个 Index 视为 int。每个集合都拥有一个 Index 属性。字典类型拥有自己的索引,通常我们并不会去使用这个属性,也无需去关心它,不过在底层它切实是存在的。一旦我们使用了 Index,那么就需要一个 startIndex 和一个 endIndex 来告诉系统该从哪里开始,以及该从哪里结束。对于数组而言,startIndex 将会 0,但是对于数组片段 (array slice) 而言,这个值很可能是别的位置。接下来,您可能希望能够在获取该索引处的元素。我们将使用 subscript 函数。您需要能够获取到当前索引之后的下一个索引。我们不必使用迭代器。Swift 已经为我们完成了这些操作。

以下是说明如何在集合中实现 forEach 的一个示例:

func forEach(_ block: (Element) -> Void) {
    var currentIndex = self.startIndex
    while currentIndex < self.endIndex {
        block(self[currentIndex])
        currentIndex = self.index(after: currentIndex)
    }
}

您完全没必要去实现这个方法;它已经内置在标准库当中了,但是这里是它的实现方式。您将从当前索引开始,并检查当前索引是否小于 endIndex。这就是为什么 Index 这个关联类型必须是可比较的 (Comparable):您需要检查它是否小于 endIndex。您将使用当前正在查看的值来调用这个闭包,然后更新当前值从而去查看下一个索引。这个过程将一直持续,直到抵达 endIndex,这个时候迭代就会终止掉。这就是我们从 Sequence 中获得的功能,这些函数全部已经内置实现了。


extension APIErrorCollection: Collection {

    var startIndex: Int {

        return _errors.startIndex

    }

    var endIndex: Int {

        return _errors.endIndex

    }

    func index(after index: Int) -> Int {

        return _errors.index(after: index)
    }

    subscript(position: Int) -> APIError {

        return _errors[position]

    }

}

通常而言,我并不会从零开始去构建集合,但是有一些类型我还是很希望它们能拥有和集合一样的功能的。举个例子,我可能需要一个 API 错误集合。其内部包含了一个 APIError 属性的数组,但是我想要实现的功能是,我想要让错误集合能够拥有集合的各项功能。但是我并不能这么做,因为错误属性是私有的(我没有办法查看其中的内容)。因此我想要实现自己的 API 错误集合,然后让其拥有集合的功能。我能够很轻易地获取这些功能。


struct APIErrorCollection {

    fileprivate let _errors: [APIError]

}

extension APIErrorCollection: Collection {

    // ...

}

errorCollection.contains({ $0.id == "error.item_already_added" })

    // compiles!

为此,我们需要实现一个扩展,或者我们需要让我们的 APIErrorCollection 实现 Collection 协议。因为我们已经有一个内部属性,它已经完全符合 Collection 协议的要求,因此我们可以通过转发来实现这个功能。startIndex 将变成 errors.startIndexendIndex 将变成 errors.endIndex 等等。将索引进行转发之后,我们需要对下标 (subscript) 进行转发。这很直接明了。借此我们成功让不是数组的东西表现得和数组一样。现在我们的代码就完成了,如果我们想检查是否包含了某种错误,那么借助 Collection 协议就可以完成了。此外我们也实现了 Sequence 协议。我们能够很轻松地通过内置的这个属性,得到 mapfilter 等这些功能。

双向集合

双向集合与集合非常类似,只是它多了一个功能。与 Collection 继承自 Sequence 相同,BidirectionalCollection 继承自 Collection,但是双向集合可以向两个方向任意移动。在集合当中,前进我们已经有了 indexAfter 这个函数,所以为了增加后退的功能,您需要再增加一个函数。在双向集合中,我们将添加一个名为 indexBefore 的新函数,它将可以让我们以相反的顺序来遍历集合。


protocol Collection {

  //...

  func index(after index: Index) -> Index

}


protocol BidirectionalCollection: Collection {

  func index(before index: Index) -> Index

}

我们能从双向集合中轻松得到的一个功能就是这个属性 last。通过这个属性,我们将能够得到给定双向集合当中的最后一个元素,如果集合为空的话将得到 nil。我们无法在集合中实现这个属性,因为我们必须要一直走到集合的末尾才能返回最后一个元素。这需要太长的时间了,而我们希望能直接一步跳到最后,并立即将末尾的值返回。这里我们需要检查集合是否为空。如果集合为空,那么简单的返回 nil 就可以,我们就知道工作已经结束了。如果不是,我们则需要去取 endIndex,然后得到前一个索引。因为 endIndex 指向的是最后一个元素的next 值。最后,我们将这个索引对应的值返回即可。


var last: Iterator.Element? {

    guard !self.isEmpty else { return nil }

    let indexOfLastItem = self.index(before: self.endIndex)

    return self[indexOfLastItem]

}

双向协议让我们能够轻松地取得上一个元素,因为我们可以后向遍历。这使得集合的功能更加完善。

其它协议

如果大家有兴趣的话,还可以去看看另外三个协议:

  • RandomAccessCollection:可以更快地访问值。您可以直接跳转到想要获取的那个元素,而不必去逐步遍历。
  • RangeReplaceableCollection:允许您使用其它内容来将集合中间的某个部分替换掉。
  • MutableCollection:允许您对这些值进行读取或修改。

如果您在看 Swift 文档或者 Swiftdoc.org 的话,那么您可以去看一看这些协议的实现方式,看看有哪些新功能您需要去实现,希望您能够借助从我这里学到的东西,能够更好地去了解这些协议。

问答时间到!

问:您说 Swift 中的这个 Iterator 类型增加了语言的表达力,那么这些增加的表达力表现在何处呢?

Soroush:在您构建内容的大多数时候,Iterator 类型通常都只在内部起作用,或者说通常只有 Swift 标准库才会使用它。您完全不用去在乎这个类型,但是有时借助这个类型您可以去手动执行某些操作,比如说我们假设您正在几个不同的页面之间来回切换,那么每次您可以选择去持有这些页面的迭代器,而不是选择去持有这些页面的集合。如果持有的是迭代器的话,那么每次您切换页面的时候,您只需要调用 next 即可,它就能够为您展示下一个元素,以此类推,这样您就可以借助 Iterator 类型来增强代码的表达力。

About the content

This talk was delivered live in March 2017 at try! Swift Tokyo. The video was recorded, produced, and transcribed by Realm, and is published here with the permission of the conference organizers.

Soroush Khanlou

Soroush Khanlou is a New York-based iOS consultant. He’s written apps for the New Yorker, David Chang’s Ando, Rap Genius, and non-profits like Urban Archive. He blogs about programming at khanlou.com, mostly making fun of view controllers. In his free time, he runs, bakes bread and pastries, and collects suitcases.

4 design patterns for a RESTless mobile integration »

close