美文网首页
Swift5 讨论Iterator的行为特征

Swift5 讨论Iterator的行为特征

作者: 醉看红尘这场梦 | 来源:发表于2019-12-14 20:18 被阅读0次

这一节,我们专门来讨论Iterator。而故事的开始,就是上一节留下的一个问题:为什么IteratorProtocol中的next方法要约束成mutating呢?

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

Iterator是一个值类型

这个问题的答案,可以从Iterator被设计出来的初衷以及具体的实现方式两方面来说。一方面,获取Iterator的目的,就是希望通过它依次遍历序列中的元素,在绝大多数场景里,Iterator都是一个独立工作的对象。很难想象我们会用多个彼此共享当前遍历状态的Iterator“轮番”遍历同一个序列的场景。另一方面,通过上一节的实现我们也看到了,Iterator的状态属于对象的“隐私”,在遍历的过程中,我们无法也无需去了解Iterator究竟用什么在维系当前的遍历。对于这样一个不透明的属性,让多个Iterator共享,我们几乎无法预期遍历的结果。

正是出于这两点考虑,Iterator几乎都是通过struct定义的值类型。但在遍历的过程中,Iterator又必须改变其内部的遍历状态。这也就是为什么next要定义成mutating的原因了。

当我们拷贝一个Iterator的时候,Iterator在内部维系的状态会被一并拷贝。这两个Iterator对Sequence的遍历,是彼此互不干扰的。

先来看个Swift标准库的例子:

let arr = [1, 2, 3, 4, 5, 6]

var ia = arr.makeIterator()
var ia1 = ia

ia.next()    // 1
ia.next()    // 2
ia1.next()   // 1
ia1.next()   // 2

再来看个我们之前自定义过的序列:

var fibA = FibonacciIterator()
var fibA1 = fibA

fibA.next()    // 0
fibA.next()    // 1
fibA1.next()   // 0
fibA1.next()   // 1

这两个例子有一个共性,就是它们都是可以重复遍历的序列。对这种情况,复制Iterator,就好比在序列的某个位置打了一个标记,以便我们接下来从这个位置开始继续遍历。

那对于不可重复遍历的序列,复制Iterator又意味着什么呢?例如,对于上一节最后的例子:

let stdin = InputStream()
var iter1 = stdin.makeIterator()
var iter2 = iter1

while true {
  guard let _ = iter1.next(),
    let _ = iter2.next() else { break }
}

这里的var iter2 = iter1意味着什么呢?如果你觉得难以想象,不妨重新执行一下这个demo,这次,你需要在控制台上输入两次之后,才能看到输出结果。类似下面这样:

>> ha
(1) ha
>> ha
(1) ha
>> haha
(2) haha
>> haha
(2) haha

第一次输入,iter1遍历到了序列的第一个结果。第二次输入,iter2才遍历到序列的第一个结果。从“上帝视角”看下去,此时的iter1和iter2是在遍历两个不同的序列。而造成这种错觉的原因,是因为InputStream是一个不可重复遍历的序列,每一次next都会消耗掉序列中的元素。

而从iter1和iter2各自的视角看,它们完全感知不到其它Iterator的存在。它们能感知到的,就是在从创建它们开始表达的位置,逐个向后遍历同一个序列罢了,这可以从iter1和iter2打印结果里小括号中的数字确认到。每一个Iterator都认为,自己遍历的,是全部的用户输入。所以,从语义上说,Iterator的行为和之前的两个例子,是一致的。

使用Any封装Iterator和Sequence
但如果,我们就是要创建引用语义的Iterator怎么办呢?对于InputStream这种消耗型的序列来说,让所有iterators共享遍历状态往往会让结果更符合我们对序列迭代行为的预期。为此,Swift标准库中提供了一个普通Iterator的包装,叫做AnyIterator<T>,其中T表示序列中元素的类型。创建一个AnyIterator很简单,把值类型的普通Iterator传给它就好了。我们把InputStream的makeIterator改成下面这样:

func makeIterator() -> AnyIterator<String> {
  return AnyIterator(InputStreamIterator())
}
``

重新执行一下,你猜会得到什么结果呢?执行一下就会看到,是类似这样的:

>> ha
(1) ha
>> haha
(2) haha
>> hahaha
(3) hahaha
>> hahahaha
(4) hahahaha
从前面小括号里的值就能确认,iter1和iter2是共享遍历状态的。这样,也就不会有之前iter1和iter2遍历两个不同序列的错觉了。

除了这个特别的引用语义之外,AnyIterator还有一个作用,就是隐藏一个Sequence中Iterator的细节。如果一个Sequence的Iterator过于复杂,或者在未来有可能发生变化,就可用AnyIterator把它包装起来。对比一下InputStream中makeIterator的两个实现,就能明白这个含义了:

func makeIterator() -> InputStreamIterator {
  return InputStreamIterator()
}

func makeIterator() -> AnyIterator<String> {
  return AnyIterator(InputStreamIterator())
}
除了明确使用一个普通的Iterator创建AnyIterator之外,Swift还提供了一种“匿名”的形式。直接用closure替代普通Iterator中的next方法:

AnyIterator {
  guard let input = readLine() else { return nil }

  currLine += 1
  print("(\(currLine)) \(input)")

  return input
}
用这样的方式创建出来的AnyIterator功能上和AnyIterator(InputStreamIterator())是相同的。有时候我们就是想要一个Iterator生成数据,而根本不关心名字,这种用法就会很方便。

AnySequence
既然有AnyIterator,就不难想到,还应该有一个AnySequence。创建它的方式和AnyIterator是类似的,直接给它一个普通的Sequence就行了。大家先知道有这么个用法就好,在后面的内容中,我们会专门讨论这个类型的实现:

let stdin = AnySequence(InputStream())

相关文章

网友评论

      本文标题:Swift5 讨论Iterator的行为特征

      本文链接:https://www.haomeiwen.com/subject/qhjhnctx.html