回答了“暴露哪些和容器自身有关的类型”这个问题之后,这一节,我们逐个看看Sequence
类型的实现细节。
提供哪些和尺寸有关的接口
对于任意一个表示容器概念的类型来说,一定都会提供一些和“尺寸”有关的接口。这类接口通常回答两个问题:容器内当前有多少个元素;容器在重新调整容量之前最多可以容纳多少个元素。由于Sequence
有可能是无限序列,它只提供了一个只读属性:underestimateCount
:
/// A value less than or equal to the number of elements in the sequence,
/// calculated nondestructively.
///
/// The default implementation returns 0\. If you provide your own
/// implementation, make sure to compute the value nondestructively.
///
/// - Complexity: O(1), except if the sequence also conforms to `Collection`.
/// In this case, see the documentation of `Collection.underestimatedCount`.
var underestimatedCount: Int { get }
从注释中可以看到:
- 这是一个小于等于当前序列內元素个数的值,默认是0;
- 获取这个值的过程不应该导致序列不可用,也就是它不应该消费掉序列中的元素;
- 获取这个值的算法复杂度应该是
O(1)
;
因此,从这些要求来看,这个值对Sequence
来说,应该不是为了精确计数的,而应该是为了在一些特定情况下Sequence
类型的实现机制服务的。
有哪些直接访问元素的方法
这里我们说的直接访问元素的方法,指的是可以直接获得序列中Element
对象的方法。由于Sequence
有可能是无限序列,并且有可能仅支持单次遍历,因此,和对Iterator
的约束相同,我们不应该提供任何获取随机位置的元素访问方法。实际上,Swift也的确没有为Sequence
约束相关的接口。
可以获取哪些形式的Iterator
于是,访问Sequence
中元素的唯一方式,就是通过与之搭配的Iterator
了。之前我们已经说过,Sequence
约束了一个接口makeIterator
:
public protocol Sequence {
func makeIterator() -> Iterator
}
同样,由于Sequence
是只读的,还可能是无限的,我们只能获取这种单步向前的只读Iterator
,并通过它间接获取序列中的元素。没有逆序遍历的Iteartor
也没有可以修改元素的Iterator
。
支持哪些形式的比较
提起比较,通常意味着两方面的问题,一个是范围,一个是标准。由于序列可能是无限的,当我们在程序库设计者的立场讨论Sequence
的比较问题时,既不能假定是比较全部元素,也不能假定就是基于数字的大于、小于和等于。通常,在这方面的设计上,都会采用重载函数的机制。我们用比较两个Sequence
是否相等来举例,这部分内容,是以“算法”的身份出现的,它定义在SequenceAlgorithms.swift里。
先来看这个比较相等的一般实现:
extension Sequence {
///
/// ...
/// At least one of the sequences must be finite.
/// ...
///
@inlinable
public func elementsEqual(
_ other: OtherSequence,
by areEquivalent: (Element, OtherSequence.Element) throws -> Bool
) rethrows -> Bool {
var iter1 = self.makeIterator()
var iter2 = other.makeIterator()
while true {
switch (iter1.next(), iter2.next()) {
case let (e1?, e2?):
if try !areEquivalent(e1, e2) {
return false
}
case (_?, nil), (nil, _?): return false
case (nil, nil): return true
}
}
}
}
在它的注释里,我摘录了其中比较重要的一个限制,就是两个进行比较的序列中,至少有一个是有限序列。然后,在它的实现里:
首先,是开头的@inlinable
,这种用@
开头的标记叫做attribute。在Swift的源代码里,存在着各种attribute。我们随着在源代码中遇到一个,就来说一下它的用途。这里,@inlineable
可以帮助编译器把函数body作为module的接口导出,这样,当我们调用一些标准库的函数时,就可以获得额外的性能提升,关于这部分的设计,大家可以参考SE-0193。由于我们对内联的概念都不陌生,因此也就不再多说它了。
其次,是它的声明:
public func elementsEqual(
_ other: OtherSequence,
by areEquivalent: (Element, OtherSequence.Element) throws -> Bool
) rethrows -> Bool
在上面的代码里可以看到,elementsEqual
用了另外一个泛型参数OtherSequence
表示与原始Sequence
进行比较的类型,这说明什么呢?其实,最重要的一点就是:进行比较的两个Sequence中元素的类型可以是不一致的。这通过它的第二个参数areEquivalent
就可以明确看出来了,这个实际进行比较的函数的第一个参数是Element
,第二个参数是OtherSequence.Element
,然后,他返回一个Bool
表示比较的结果。而这就是我们刚才说过的,通过一个函数抽象比较操作的行为。
接下来,就是它的定义本身了。和我们在上一节看到的reduce1
一样,elementsEqual
的实现中没有任何与Sequence
实现细节有关的内容,要比较的元素都是通过Iterator
取得的,实际的比较是通过函数参数完成的。而这个算法,仅仅是表达了逐个比较两个Sequence
中的元素,然后根据结果进行判断的逻辑而已。
有了这个一般的elementsEqual
之后,为了方便程序库的使用,通常我们会通过限定一些条件,定义一个最常用的版本。当然,Swift也是这么做的:
extension Sequence where Element : Equatable {
@inlinable
public func elementsEqual(
_ other: OtherSequence
) -> Bool where OtherSequence.Element == Element {
return self.elementsEqual(other, by: ==)
}
}
可以看到,当Sequence
中的元素支持比较操作,并且,进行比较的两个Sequence
中元素类型相同时,这个重载版本的elementsEqual
默认使用了==
进行比较。而我们不妨按照这个“严格”的约束,给Sequence
扩展出熟悉的相等比较操作:
extension Sequence where Element : Equatable {
static func ==(
lhs: Self, rhs: OtherSequence) -> Bool
where Element == OtherSequence.Element {
return lhs.elementsEqual(rhs)
}
}
这样,就实现了只有两个Sequence
元素类型相同,元素个数相同,每个位置上元素都相等,并且至少有一个为有限序列的时候,两个Sequence
是相等的判断方法。但是,为什么标准库中没有为Sequence
提供==
比较呢?答案很简单,就是这些限制附加与==
操作符的时候,太过于隐晦了。我们在使用==
的时候,很少会假定参与比较的成员要遵从这么多藏于幕后的条件。而为自定义类型重载操作符的一个最基本的准则就是:让它用起来就像我们对它的直觉一样。因此,Swift标准库没有提供它,而是提供了语义更清晰的elementsEqual
方法,它明确告诉开发者,比较的是两个序列中的元素是否相等。
除了比较两个Sequence
是否相等之外,标准库还提供了另外两个比较操作:
- 一个是
starts
,它的定义在这里,用于比较一个Sequence
是否以另外一个Sequence
开头; - 另一个是
lexicographicallyPrecedes
,它的定义在这里,用于按照字典顺序比较两个Sequence
中对应位置的元素;
这两个方法和elementsEqual
一样,也是由一个一般化和一个特殊化的重载方法实现的,实现的逻辑和elementsEqual
非常类似,大家可以作为练习自己去分析一下,我们就不再重复了。因此,Sequence
一共提供了3种形式的比较,共6个API。
现在,只剩下最后一个问题了:提供哪些以只读方式访问元素的接口。所谓“只读”指的是,这些API提供的逻辑,最终都会返回一个新的Sequence
,而不会修改原有Sequence
中的数据。而为了回答这个问题,我们还要进一步把这个“只读方式”细化一下。
遍历Sequence
我们要研究的第一类只读操作,是遍历Sequence
。Swift提供了两个方法:forEach
和enumerated
。当然,它们都应该应用在有限序列上。
先来看forEach
,它的定义在这里:
extension Sequence {
@inlinable
public func forEach(
_ body: (Element) throws -> Void
) rethrows {
for element in self {
try body(element)
}
}
}
可以看到,它的实现简单得不能再简单了,forEach
其实就是for
循环的一个封装,而这两种用法唯一的区别,就是我们没办法通过break
中断forEach
的遍历过程。
接下来,再来看enumerated
,它的定义在这里:
extension Sequence {
@inlinable
public func enumerated() -> EnumeratedSequence {
return EnumeratedSequence(_base: self)
}
}
接着,EnumeratedSequence
的定义在这里,它只是基于Self
的值生成的另外一个Sequence
而已。
@_fixed_layout
public struct EnumeratedSequence {
@usableFromInline
internal var _base: Base
/// Construct from a `Base` sequence.
@inlinable
internal init(_base: Base) {
self._base = _base
}
}
extension EnumeratedSequence: Sequence {
/// Returns an iterator over the elements of this sequence.
@inlinable
public func makeIterator() -> EnumeratedIterator {
return EnumeratedIterator(_base: _base.makeIterator())
}
}
基本就是Sequence
实现的标准套路,Base
指的是要“遍历”的原始Sequence
。这里,我们又遇到了一个新的attribute:@_fixed_layout
。关于这个属性的记录并不多,我只能从attr_fixed_layout.swift中摸到一些功能。简单来说,就是告诉编译器,public
权限修饰的类型,使用固定的内存布局。
接下来,我们再来看下EnumeratedIterator
,它的定义在这里:
@_fixed_layout
public struct EnumeratedIterator {
@usableFromInline
internal var _base: Base
@usableFromInline
internal var _count: Int
/// Construct from a `Base` iterator.
@inlinable
internal init(_base: Base) {
self._base = _base
self._count = 0
}
}
extension EnumeratedIterator: IteratorProtocol, Sequence {
public typealias Element = (offset: Int, element: Base.Element)
@inlinable
public mutating func next() -> Element? {
guard let b = _base.next() else { return nil }
let result = (offset: _count, element: b)
_count += 1
return result
}
}
在上面的代码里:
第一个要说的,是这个@usableFromInline
,我们可以在这里找到关于这个属性的讨论。简单来说,@usableFromInline
可以把标记的方法作为Swift module二进制接口的一部分,供被标记为@inlinable
的方法调用。但@usableFromInline
标记的方法不会作为module的源代码级别的接口。因此,这个属性只能用于internal
访问权限的接口。
第二个要说的是它的定义,可以看到,它有两个属性:_base
是它封装的Iterator
,_count
则是一个初始值为0的Int
。并且,EnumeratedIterator
基于Base.Element
重定义了自己的Element
类型:(offset: Int, element: Base.Element)
最后,是它的next
方法。看到这个实现,我们就明白了。当我们通过for
遍历enumerated
返回结果的时候,每次得到的offset
不一定代表着元素在集合中的位置。因为它其实就是一个从0增长的整数而已。
对于Array
或者ContiguousArray
这种索引从0自然增长的集合来说,offset
对应的就是每一个元素在集合中的索引。但是对于Set
这种非线性集合,offset
实际上没有什么明确的意义。