购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

3.1 序列

Sequence协议是集合类型结构中的基础。一个序列(sequence)代表的是一系列具有相同类型的值,你可以对这些值进行迭代。遍历一个序列最简单的方式是使用for循环:

for element in someSequence{

doSomething(with:element)

}

Sequence协议提供了许多强大的功能,满足该协议的类型都可以直接使用这些功能。上面这样步进式地迭代元素的能力看起来十分简单,但它却是Sequence可以提供这些强大功能的基础。在上一章中已经提到过不少这类功能了,每当你遇到一个能够针对元素序列进行的通用的操作,你都应该考虑将它实现在Sequence层的可能性。在接下来的部分,会看到许多这方面的例子。

满足Sequence协议的要求十分简单,你需要做的所有事情就是提供一个返回 迭代器 (iterator)

的makeIterator()方法:

protocol Sequence{

associatedtype Iterator:IteratorProtocol

func makeIterator()->Iterator

}

对于迭代器,我们现在只能从Sequence的定义中知道它应该是一个满足IteratorProtocol协议的类型。所以首先来仔细看看迭代器是什么。

迭代器

序列通过创建一个迭代器来提供对元素的访问。迭代器每次产生一个序列的值,并且在遍历序列时对遍历状态进行管理。在IteratorProtocol协议中唯一的一个方法是next(),这个方法需要在每次被调用时返回序列中的下一个值。当序列被耗尽时,next()应该返回nil:

protocol IteratorProtocol{

associatedtype Element

mutating func next()->Element?

}

关联类型Element指定了迭代器产生的值的类型。比如String.CharacterView的迭代器的元素类型是Character。通过扩展,迭代器同时也定义了它对应的序列的元素类型;当你在处理Sequence的函数签名或者泛型约束的时候,你经常会见到Iterator.Element,实际上这里的Element就是IteratorProtocol中所定义的。在本章以及接下来的协议中会介绍很多关于带有关联类型的协议的内容。

一般来说我们只有在想要实现一个自己的自定义序列类型时,才需要关心迭代器。除此之外,你几乎不会直接去使用它,因为for循环才是遍历序列常用的方式。实际上,这正是for循环背后帮我们所做的事情:编译器会为序列创建一个新的迭代器,并且不断调用迭代器的next方法,直到它返回nil为止。从本质上说,我们在上面看到的for循环其实是下面这段代码的一种简写形式:

var iterator=someSequence.makeIterator()

while let element=iterator.next(){

doSomething(with:element)

}

迭代器是单向结构,它只能按照增加的方向前进,而不能倒退或者重置。虽然大部分的迭代器的next()都只产生有限数量的元素,并最终会返回nil,但是你也完全可以创建一个无限的、永不枯竭的序列。实际上,除了那种一上来就返回nil的迭代器,最简单的情况应该是一个不断返回同样值的迭代器了:

struct ConstantIterator:IteratorProtocol{

typealias Element=Int

mutating func next()->Int?{

return 1

}

}

显示地使用typealias指定Element的类型其实并不是必需的(不过通常可以用为文档的目的帮助理解代码,特别是在更大的协议中这点尤为明显)。如果我们去掉它,则编译器也会从next()的返回值类型中推断出Element的类型:

struct ConstantIterator:IteratorProtocol{

mutating func next()->Int?{

return 1

}

}

注意,在这里next()被标记为mutating。对这个简单的例子来说,我们的迭代器不包含任何可变状态,所以它并不是必需的。不过在实践中,迭代器的本质是存在状态的。几乎所有有意义的迭代器都会要求可变状态,这样它们才能够管理在序列中的当前位置。

现在,我们可以创建一个ConstantIterator的实例,并使用while循环来对它产生的序列进行迭代,这将会打印无穷的数字1:

var iterator=ConstantIterator()

while let x=iterator.next(){

print(x)

}

我们来看一个更有意义的例子。FibsIterator迭代器可以产生一个斐波那契序列 [1] 。它将记录接下来的两个数字,并作为状态存储,next函数做的事情是为接下来的调用更新这个状态,并且返回第一个数。和之前的例子一样,这个迭代器也将产生“无穷”的数字,它将持续累加数字,直到程序因为所得到的数字发生类型溢出而崩溃(我们暂时先不考虑这个问题):

struct FibsIterator:IteratorProtocol{

var state=(0,1)

mutating func next()->Int?{

let upcomingNumber=state.0

state=(state.1,state.0+state.1)

return upcomingNumber

}

}

遵守序列协议

我们也可以创造有限序列的迭代器,比如下面这个PrefixGenerator就是一个例子。它将顺次生成字符串的所有前缀(也包含字符串本身)。它从字符串的开头开始,每次调用next时,会返回一个追加之后一个字符的字符串切片,直到达到整个字符串尾部为止:

struct PrefixIterator:IteratorProtocol{

let string:String

var offset:String.Index

init(string:String){

self.string=string

offset=string.startIndex

}

mutating func next()->String?{

guard offset<string.endIndex else{return nil}

offset=string.index(after:offset)

return string[string.startIndex..<offset]

}

}

(string[string.startIndex..<offset]是一个对字符串的切片操作,它将返回从字符串开始到偏移量为止的子字符串。后面会再对切片进行一些讨论。)

有了PrefixIterator,定义一个PrefixSequence类型就很容易了。和之前一样,我们不需要指明关联类型Iterator的具体类型,因为编译器可以从makeIterator方法的返回类型中自己推断出来:

struct PrefixSequence:Sequence{

let string:String

func makeIterator()->PrefixIterator{

return PrefixIterator(string:string)

}

}

现在,我们可以使用for循环来进行迭代,并打印出所有的前缀了:

for prefix in PrefixSequence(string:"Hello"){

print(prefix)

}

/*

H

He

Hel

Hell

Hello

*/

我们还可以对它执行Sequence提供的所有操作:

PrefixSequence(string:"Hello").map{$0.uppercased()}

//["H","HE","HEL","HELL","HELLO"]通过同样的方式,我们可以为ConstantIterator和FibsIterator创建对应的序列。我们在这里就不进行展示了,你可以自己尝试一下。不同之处在于,这些迭代器会创建出无穷序列,你可以使用像是for i in fibsSequence.prefix(10)的方式来截取其中有限的一段进行测试。

迭代器和值语义

我们至今为止所看到的迭代器都具有值语义。如果将迭代器复制一份,则迭代器的所有状态也都会被复制,这两个迭代器将分别在自己的范围内工作,这是我们所期待的。标准库中的大部分迭代器也都具有值语义,不过也有例外存在。

为了说明值语义和引用语义的不同,首先来看看StrideToIterator的例子。它是stride(from :to:by:)方法返回的序列所使用的迭代器。可以创建一个StrideToIterator并试着调用几次next方法:

//一个从0到9的序列

let seq=stride(from:0,to:10,by:1)

var i1=seq.makeIterator()

i1.next()//Optional(0)

i1.next()//Optional(1)

现在i1已经准备好返回2了。下面对它进行复制:

var i2=i1

现在原有的迭代器和新复制的迭代器是分开且独立的了,在下两次next时,它们分别都会返回2和3:

i1.next()//Optional(2)

i1.next()//Optional(3)

i2.next()//Optional(2)

i2.next()//Optional(3)

这是因为StrideToIterator是一个很简单的结构体,它的实现和上面提到的斐波纳契迭代器没有太大不同,它也具有值语义。

现在来看一个不具有值语义的迭代器的例子。AnyIterator是一个对别的迭代器进行封装的迭代器,它可以用来将原来的迭代器的具体类型“抹消”掉。比如你在创建公有API时想要将一个很复杂的迭代器的具体类型隐藏起来,而不暴露它的具体实现时,就可以使用这种迭代器。AnyIterator进行封装的做法是将另外的迭代器包装到一个内部的对象中,而这个对象是引用类型。(如果你想要了解具体做了什么,可以看看协议一章中关于类型抹消部分的内容。)

这造成了行为上的不同。我们创建一个包装了i1的AnyIterator,然后进行复制:

var i3=AnyIterator(i1)

var i4=i3

在这种情况下,原来的迭代器和复制后的迭代器并不是独立的,因为实际的迭代器不再是一个结构体,AnyIterator并不具有值语义。AnyIterator中用来存储原来迭代器的盒子对象是一个类实例,当将i3赋值给i4的时候,只有对这个盒子的引用被复制了。盒子里存储的对象将被两个迭代器所共享。所以,任何对i3或者i4进行的调用,都会增加底层那个相同的迭代器的取值:

i3.next()//Optional(4)

i4.next()//Optional(5)

i3.next()//Optional(6)

i3.next()//Optional(7)

显然,这可能会造成一些bug,不过在实践中你可能很少会遇到这种问题。这是因为平时你一般不会在代码里用迭代器这样来回传递赋值,基本上会在本地创建迭代器,这种创建有时候是显式进行的,但更多的时候是通过使用for循环隐式地创建的。你只用它来循环元素,然后就将其抛弃。如果你发现你要与其他对象共享一个迭代器,则可以考虑将它封装到序列中,而不是直接传递它。

基于函数的迭代器和序列

AnyIterator还有另一个初始化方法,那就是直接接受一个next函数作为参数。与对应的AnySequence类型结合起来使用,我们可以做到不定义任何新的类型,就能创建迭代器和序列。举个例子,我们可以通过一个返回AnyIterator的函数来定义斐波纳契迭代器:

func fibsIterator()->AnyIterator<Int>{

var state=(0,1)

return AnyIterator{

let upcomingNumber=state.0

state=(state.1,state.0+state.1)

return upcomingNumber

}

}

通过将state放到迭代器的next函数外面,并在闭包中将其进行捕获,闭包就可以在每次被调用时对其进行更新。这里的定义和上面使用自定义类型的斐波纳契迭代器只有一个功能上的不同,那就是自定义的结构体具有值语义,而使用AnyIterator定义的没有。

因为AnySequence提供了一个初始化方法,可以接受返回值为迭代器的函数作为输入,所以创建序列就非常容易了:

let fibsSequence=AnySequence(fibsIterator)

Array(fibsSequence.prefix(10))//[0,1,1,2,3,5,8,13,21,34]

另一种方法是使用Swift3中引入的sequence函数。这个函数有两种版本。首先sequence( first:next:)将使用第一个参数的值作为序列的首个元素,并使用next参数传入的闭包生成序列的后续元素。在下面的例子中,我们生成了一个随机数的序列,其中每个元素都要比前一个小,直到到达0为止:

let randomNumbers=sequence(first:100){(previous:UInt32)in

let newValue=arc4random_uniform(previous)

guard newValue>0 else{

return nil

}

return newValue

}

Array(randomNumbers)//[100,83,27,13,10,2,1]

另一个版本是sequence(state:next:),因为它可以在两次next闭包被调用之间保存任意的可变状态,所以它更强大一些。我们可以用它来通过以及一个单一的方法调用来构建出斐波纳契序列:

let fibsSequence2=sequence(state:(0,1)){

//在这里编译器需要一些类型推断的协助

(state:inout(Int,Int))->Int?in

let upcomingNumber=state.0

state=(state.1,state.0+state.1)

return upcomingNumber

}

Array(fibsSequence2.prefix(10))//[0,1,1,2,3,5,8,13,21,34]

sequence(first:next:)和sequence(state:next:)的返回值类型是UnfoldSequence。这个术语来自函数式编程,在函数式编程中,这种操作被称为展开(unfold)。sequence是和reduce对应的(在函数式编程中reduce又常被叫作fold)。reduce将一个序列缩减(或者说折叠)为一个单一的返回值,而sequence则将一个单一的值展开形成一个序列。

这两个sequence方法非常有用,它们经常用来代替传统的C语言风格的循环,特别是当下标的步长不遵守线性关系的时候。

无限序列

就像我们至今为止看到的迭代器一样,sequence对于next闭包的使用是被延迟的。也就是说,序列的下一个值不会被预先计算,它只在调用者需要的时候生成。这使得我们可以使用类似fibsSequence2.prefix(10)这样的语句,因为prefix(10)只会向序列请求它的前十个元素,然后停止。如果序列是主动计算它的所有值,因为序列是无限的,则程序将会在有机会执行下一步之前就因为整数溢出的问题而发生崩溃。

对序列和集合来说,它们之间的一个重要区别就是序列可以是无限的,而集合则不行。

不稳定序列

序列并不只限于像是数组或者列表这样的传统集合数据类型。像网络流、磁盘上的文件、UI事件的流,以及其他很多类型的数据都可以使用序列来进行建模。但是这些都和数组不太一样,对于数组,可以多次遍历其中的元素,而在上面这些例子中我们并非对所有的序列都能这么做。

斐波纳契序列确实不会因为遍历其中的元素而发生改变,我们可以从0开始再次进行遍历,但是像是网络包的流这样的序列将会随着遍历被消耗。就算再次对其进行迭代,它也不会再次产生同样的值。两者都是有效的序列,Sequence的文档非常明确地指出了 [2] 序列并不保证可以被多次遍历:

Sequence协议并不关心遵守该协议的类型是否会在迭代后将序列的元素销毁。也就是说,请不要假设对一个序列进行多次的for-in循环将继续之前的循环迭代或者从头再次开始:

for element in sequence{

if...some condition{break}

}

for element in sequence{

//未定义行为

}

一个非集合的序列可能会在第二次for-in循环时产生随机的序列元素。

这也解释了为什么我们只有在集合类型上能见到first这个看起来很简单的属性,而在序列中该属性却不存在。调用一个属性的getter方法应该是非破坏的,但序列无法对此进行

保证。

举一个破坏性的可消耗序列的例子:我们有一个对readLine函数的封装,它会从标准输入中读取一行:

let standardIn=AnySequence{

return AnyIterator{

readLine()

}

}

现在,你可以使用Sequence的各种扩展来进行操作了,比如你可以这样来写一个带有行号的类型Unix中cat命令的函数:

let numberedStdIn=standardIn.enumerated()

for(i,line)in numberedStdIn{

print("\(i+1):\(line)")

}

enumerate将一个序列转化为另一个带有递增数字的新序列。和readLine进行的封装一样,这里的元素也是 延迟 生成的。对于原序列的消耗只在你通过迭代器移动到被迭代的序列的下一个值时发生,而不是在序列被创建时发生的。因此,在命令行中运行上面的代码时,会看到程序在for循环中进行等待。当输入一行内容并按下Enter键的时候,程序会打印出相应的内容。当按下Control-D组合键结束输入的时候,程序才会停止等待。不过无论如何,每次enumerate从standardIn中获取一行时,它都会消耗掉标准输入中的一行。你没有办法将这个序列迭代两次并获得相同的结果。

如果你在写一个Sequence的扩展,则并不需要考虑这个序列在迭代时是不是会被破坏。但是如果你是一个序列类型上的方法的 调用者 ,那么你应该时刻提醒自己注意访问的破坏性。如果一个序列遵守Collection协议的话,那就可以肯定这个序列是稳定的了,因为Collection在这方面进行了保证。但是反过来却不一定,稳定的序列并不一定需要是满足Collection。在标准库中,就有一些非集合的序列是可以被多次遍历的。这样的例子包括stride(from: to:by:)返回的StrideTo类型,以及stride(from:through:by:)所返回的StrideThrough类型。因为你可以很取巧地使用浮点数步长来获取值,这使得它们无法被描述为一个集合,所以它们只能作为序列存在。

序列和迭代器之间的关系

序列和迭代器非常相似,你可能会问,为什么它们会被分为不同的类型?我们为什么不能直接把IteratorProtocol的功能包含到Sequence中呢?对于像前面介绍的标准输入例子那种破坏性消耗的序列来说,这么做确实没有问题。这类序列自己持有迭代状态,并且会随着遍历而发生改变。

然而,对于像斐波纳契序列这样的稳定序列来说,它的内部状态是不能随着for循环而改变的,它们需要独立的遍历状态,这就是迭代器所提供的(当然还需要遍历的逻辑,不过这部分是序列的内容)。makeIterator方法的目的就是创建这样一个遍历状态。

其实对于所有的迭代器来说,都可以将它们看作即将返回的元素所组成的不稳定序列。事实上,你可以单纯地将迭代器声明为满足Sequence来将它转换为一个序列,因为Sequence,提供了一个默认的makeIterator实现,对于那些满足协议的迭代器类型,这个方法将返回self本身。Swift团队也在swift-evolution的邮件列表里声明过,要不是由于语言限制的原因,IteratorProtocol应该继承自Sequence(具体来说,这个原因是编译器缺乏对于循环的关联类型约束的支持)。

虽然现在不可能添加这样的强制关系,不过标准库中的大部分迭代器还是满足了Sequence协议的。

子序列

Sequence还有另外一个关联类型,叫作SubSequence:

protocol Sequence{

associatedtype Iterator:IteratorProtocol

associatedtype SubSequence

//...

}

在返回原序列的切片的操作中,SubSequence被用作返回值的子类型,这类操作包括:

·prefix和suffix——获取开头或结尾n个元素

·dropFirst和dropLast——返回移除掉前n个或后n个元素的子序列

·split——将一个序列在指定的分隔元素时截断,返回子序列的数组

如果没有明确指定SubSequence的类型,那么编译器会将它推断为AnySequence<Iterator. Element>,这是因为Sequence以这个类型作为返回值,为上述方法提供了默认的实现。如果想要使用你自己的子序列类型,则必须为这些方法提供自定义实现。

在有些时候,子序列和原来序列的类型一致,也就是SubSequence==Self时,会非常方便。在标准库中,String.CharacterView就是这种情况的一个例子。在关于字符串的章节里,会展示一些例子,来说明这个特性是如何帮助我们更舒服地使用字符表示形式的。在理想世界里,SubSequence关联类型的声明应该要包括两个约束:(a)其本身也是序列,(b)子序列的元素类型和其子序列类型,要和原序列中的对应类型一致。如果加上这些约束,声明看起来会是这样的:

//Swift 3.0中无法编译

associatedtype SubSequence:Sequence

where Iterator.Element==SubSequence.Iterator.Element,

SubSequence.SubSequence==SubSequence

在Swift3.0中,编译器缺乏两个必要的特性:现在没有循环协议约束(Sequence会对自身进行引用),也没有关联类型中的where语句支持。我们希望两者都能在未来的Swift版本中引入。在这之前,我们需要自己发现这些约束条件,并将它们添加到自己的Sequence扩展方法中,否则编译器将无法理解相关类型。

下面这个例子检查了一个序列的开头和结尾是否以同样的n个元素开始。它通过比较序列的pre fi x的n个元素以及su ffi x的n个元素的逆序序列来实现。用来比较的elementsEqual方法只能在我们告诉编译器子序列也是一个序列,并且它的元素类型和原序列的元素类型相同的情况下,才能工作(序列中的类型已经遵守了Equatable):

extension Sequence

where Iterator.Element:Equatable,

SubSequence:Sequence,

SubSequence.Iterator.Element==Iterator.Element

{

func headMirrorsTail(_n:Int)->Bool{

let head=prefix(n)

let tail=suffix(n).reversed()

return head.elementsEqual(tail)

}

}

[1,2,3,4,2,1].headMirrorsTail(2)//true

在介绍泛型的时候还会介绍一个这方面的例子。 /1nIl/f+/uqThBI8Jk/erxSuJFZVIAfaScv0ab0JMzhAEu/2nWaoieO5QhfCtsic

点击中间区域
呼出菜单
上一章
目录
下一章
×