Swift源码阅读 - Sequence的实现细节

回答了“暴露哪些和容器自身有关的类型”这个问题之后,这一节,我们逐个看看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提供了两个方法:forEachenumerated。当然,它们都应该应用在有限序列上。

先来看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实际上没有什么明确的意义。

你可能感兴趣的:(Swift源码阅读 - Sequence的实现细节)