Swift5 如何表达一个最质朴的序列

Sequence是整个Swift标准库中整个集合类型体系的起点,它抽象了一个集合最朴素的形式。有多朴素呢?用文字描述出来就是:仅表示一系列类型相同的元素,而不对这一系列元素的性质有任何额外的约定。例如:是否可以多次遍历,是否可以逆序排列,甚至是否包含有限个元素等都没有约定。它唯一约定了的动作,就是从序列当前位置读取下一个元素。

从定义理解Sequence

了解了Sequence的想法之后,我们直接来看Swift中对应的实现。这部分代码在stdlib/public/core/Sequence.swift里:

public protocol Sequence {
  associatedtype Element
  func makeIterator() -> Iterator
}

实际上,在Sequence.swift开始,有一大段官方的注释解释了这个protocol设计的想法,看过这个系列的内容之后,推荐大家去读一下它们。不过现在,还是顺着我们的思路继续。可以看到,Sequence约定的内容非常简单:

  • Element表示序列中元素的类型;
  • makeIterator则返回一个Iterator对象,所谓的Iterator,可以把它看成一个了解序列内部构造的黑盒子,可以帮助我们用一致的接口获取任何序列的“下一个”元素。它的定义在这里
public protocol IteratorProtocol {
  associatedtype Element
  mutating func next() -> Element?
}

其中:

  • Element表示这个接口返回的元素类型;
  • next()方法,就是刚才说到的访问序列下一个元素的接口。由于不一定可以从序列中获取到下一个元素,因此,next的返回值是Element?。至于为什么next要约束成一个mutating方法,我们稍后用两个例子来解释。

看到这你可能会想了,Sequence.makeIterator返回的对象中Element的类型,和IteratorProtocol中的Element应该是一样的啊。事实上,的确如此,在Sequence的实现里,Iterator的定义是这样的:

public protocol Sequence {
  associatedtype Iterator : IteratorProtocol
    where Iterator.Element == Element
}

最后,把Sequence完整的定义列出来:

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

public protocol Sequence {
  associatedtype Iterator : IteratorProtocol
    where Iterator.Element == Element
  func makeIterator() -> Iterator
}

这就是Swift中,最朴实的一类集合的约定。对于一个遵从了Sequence的类型,就像我们在一开始说的一样,我们不知道它包含的元素个数、不知道它是否可以反复遍历、不知道它是否可以逆序。我们能做的,就是通过它的Iterator获取集合中的下一个元素,而这也是我们唯一可以做的事情。

尽管Swift还通过extensionSequence添加了很多方法,但它们并不是Sequence约束的一部分。也就是说,当自定义一个遵从了Sequence的类型时,不用重新实现它们。

三种不同性质的Sequence

接下来,为了更具象的了解Sequence类型的“质朴”,我们来看三个具体的例子。第一个例子,是我们最容易理解的数组。把它“降维”理解成一个序列完全没问题:

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

对于array表示的这个数字序列来说,如果不把它当数组看,我们访问它的元素就只能依靠不断调用Array.Iteratornext方法:

var ia = arr.makeIterator()

while let item = ia.next()  {
  print(item)
}

显然,这种从数组“降维”而来的序列有有限个元素并且支持重复遍历。

第二个例子,Sequence表示的序列也可以是包含无限个元素的(一直到序列中元素值溢出),例如一个Fibonacci数列。为了表示这样的类型,我们先定义对应的Iterator

struct FibonacciIterator: IteratorProtocol {
  // typealias int = Element
  var state = (curr: 0, next: 1)

  mutating func next() -> Int? {
    let curr = state.curr
    state = (curr: state.next, next: state.curr + state.next)

    return curr
  }
}

然后再来定义数列自身:

struct Fibonacci: Sequence {
  // typealias int = Element
  // typealias Iterator = FibonacciIterator

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

在注释里,我分别列出了FibonacciIteratorFibonacci中对应的associatedtype类型,为的是帮助我们理解它的定义。实际上这是不必要的,Swift可以根据makeIterator以及next的返回值推导出它们。

现在,思考一个问题,该如何表达一个Fibonacci数列呢?我给你下面两个答案,你选哪个:

/// A
var fibA = FibonacciIterator()

/// B
var fibB = Fibonacci()

是A还是B呢?其实答案是无论哪个都可以。对于fibA来说,我们的侧重点是事实,毕竟只要像下面这样就可以得到数列了:

fibA.next()
fibA.next()
fibA.next()
/// ...

而对于fibB来说,我们的侧重点是形式,Fibonacci数列是一个Sequence。按照约定,为了访问Sequence的每一个成员,我们要获取它的专属Iterator,然后通过next方法完成:

var iterB = fibB.makeIterator()
iterB.next()
iterB.next()
iterB.next()
/// ...

说到这,我们再来回答一个问题,既然FibonacciIterator自身就可以表达一个序列,为什么还要单独定义Fibonacci呢?或者说,为什么要分开IteratorSequence呢?

这个问题的答案和两个因素相关,它们是:Sequence的值和Iterator的状态。通常,Iterator为了可以持续遍历序列,它在内部都要保存一个状态。当这个状态和Sequence要表达的值一致的时候,IteratorSequence的界限就会变得模糊,甚至可以说Iterator自身就表示了这个序列。为了看到这个效果,我们可以直接让FibonacciIterator遵从Sequence

struct FibonacciIterator: IteratorProtocol, Sequence {
  /// The same as before
  /// ...
}

这样,FibonacciIterator就“名正言顺”的成为了一个序列,我们也不用为它专门编写makeIterator方法。这是因为,在Sequence代码里,Swift官方已经为这种情况提供了一个默认实现

/// A default makeIterator() function for `IteratorProtocol` instances that
/// are declared to conform to `Sequence`
extension Sequence where Self.Iterator == Self {
  /// Returns an iterator over the elements of this sequence.
  @inlinable
  public __consuming func makeIterator() -> Self {
    return self
  }
}

但绝大多数时候,Iterator保持的状态,和Sequence的值,并不是一致的。这时,就必须通过独立的类型来表达IteratorSequence了,而这就是我们要演示的第三类Sequence。这次,我们定义一个由用户输入的每一行内容形成的序列。

先来定义Iterator

struct InputStreamIterator: IteratorProtocol {
  var currLine = 0

  mutating func next() -> String? {
    guard let input = readLine() else { return nil }

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

    return input
  }
}

这次,Iterator中用于维系遍历的状态,是currLine,表示这是当前第几行输入,然后在控制台打印类似(1) Hello world这种形式的字符串,并返回当前用户输入的内容。

接下来,定义表示这个序列的类型:

struct InputStream: Sequence {
  func makeIterator() -> InputStreamIterator {
    return InputStreamIterator()
  }
}

最后,用下面的代码来测试:

let stdin = InputStream()
var stdinIter = stdin.makeIterator()

while true {
  guard let _ = stdinIter.next() else { break }
}

把这些代码保存在demo.swift里,并执行swiftc demo.swift -o demo。编译完成后,执行demo,随着在控制台中一行行输入内容,就可以看到类似下面这样的结果了:

ha
(1) ha
haha
(2) haha
hahaha
(3) hahaha
hahahaha
(4) hahahaha

通过这三个例子,我们应该可以彻底明白Sequence要表达的概念了。对于一系列类型相同的序列来说:

  • 它的值可以是预分配的,例如数组中的元素。也可以是lazy的,例如Fibonacci中的访问时现算,或者InputStream中用户的当前输入;
  • 它的元素个数可以是有限的(array),也可以是无限的(Fibonacci / InputStream);
  • 它可以被反复遍历(array / Fibonacci),或者只能遍历一次(InputStream);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容