Swift源码阅读 - Lazy机制的实现方法

上一节,我们讨论了Sequence中一些eager API的问题。而解决这个问题的办法,就是把原始的Sequence和要执行的动作保存起来,等我们真正需要其中的数据时,再“按需”调用对应的动作。顺着这个思路,我们来看Swift是如何实现的。

_SquenceWrapper

为了能把原始的Sequence“包”起来,Swift中定义了一个protocol _SequenceWrapper,它的定义在这里

@_show_in_interface
public // @testable
protocol _SequenceWrapper : Sequence {
  associatedtype Base : Sequence where Base.Element == Element
  associatedtype Iterator = Base.Iterator
  associatedtype SubSequence = Base.SubSequence

  var _base: Base { get }
}

这里,@_show_in_interface用于把_SequenceWrapper约束的接口在“某些情况”下显示在编译器生成的头文件里,大家可以在SR-984中找到一些相关的讨论,不过从阅读源代码的角度,我们还不用太在意这些细节。

接下来,在_SequenceWrapper的定义中:

  • Base:表示_SequenceWrapper“包装”的原始序列类型,可以看到,这里我们要求Base.Element_SequenceWrapper.Element是相同的;
  • IteratorSubSequence则是Sequence类型约束,显然,它们应该和Base中对应的类型是相同的;
  • 最后,_base用于返回被_SequenceWrapper“包装”的原始序列;

接下来,_SequenceWrapper通过extension为一些Sequence中的方法提供了默认实现,当然,就是把API转接到“包装过”的base

extension _SequenceWrapper {
  @inlinable // FIXME(sil-serialize-all)
  public func map<T>(
    _ transform: (Element) throws -> T
  ) rethrows -> [T] {
    return try _base.map(transform)
  }
}

这样,我们只要定义一个遵从_SequenceWrapper的类型,就有机会“缓存”对原始序列的各种操作了。

LazySequence

在上一节末尾,我们提到了Sequence中的lazy属性:

extension Sequence {
  @inlinable // FIXME(sil-serialize-all)
  public var lazy: LazySequence<Self> {
    return LazySequence(_base: self)
  }
}

接下来,我们就来看下这个LazySequence定义

@_fixed_layout // FIXME(sil-serialize-all)
public struct LazySequence<Base : Sequence>: _SequenceWrapper {
  public var _base: Base

  @inlinable // FIXME(sil-serialize-all)
  internal init(_base: Base) {
    self._base = _base
  }
}

可以看到,没什么有营养的东西。LazySequence保存了一份原始序列的拷贝,并且,通过遵从_SequenceWrapperLazySequence就有机会接管原本应该调用的_base中的方法。

接下来,LazySequence还有一个extension

extension LazySequence: LazySequenceProtocol {
  public typealias Elements = Base

  /// The `Base` (presumably non-lazy) sequence from which `self` was created.
  @inlinable // FIXME(sil-serialize-all)
  public var elements: Elements { return _base }
}

它暴露了两个信息:一个是Elements类型的属性elements;一个是LazySequenceProtocol。为什么要在这个extension中定义一个和Base一样的类型呢?这个element属性又是做什么的呢?LazySequenceProtocol又约束了什么呢?

LazySequenceProtocol

直接来看它的定义:

public protocol LazySequenceProtocol : Sequence {
  associatedtype Elements : Sequence = Self
    where Elements.Iterator.Element == Iterator.Element

  var elements: Elements { get }
}

extension LazySequenceProtocol {
  @inlinable // FIXME(sil-serialize-all)
  public var lazy: LazySequence<Elements> {
    return elements.lazy
  }
}

extension LazySequenceProtocol
  where Elements: LazySequenceProtocol {
  @inlinable // FIXME(sil-serialize-all)
  public var lazy: Elements {
    return elements
  }
}

extension LazySequenceProtocol where Elements == Self {
  /// Identical to `self`.
  @inlinable // FIXME(sil-serialize-all)
  public var elements: Self { return self }
}

可以看到,实际上LazySequenceProtocol只定义了两个约束,分别是:elementslazy。其中,elements是原始的序列,而lazy则表示elements被“延迟处理”之后的序列。正是这两个属性,约定了我们访问“普通版”和“延迟版”序列的方法。这样,我们也就不难理解LazySequence的定义了。

另外,LazySequenceProtocol还为一些特殊情况定义了elementslazy的默认实现:

  • 当要封装的序列自身就遵从LazySequenceProtocol时,它的lazy属性直接返回封装的序列自身就好了,就没必要再用LazeSequence封装一层了;
  • 当要封装的序列和遵从LazySequenceProtocol的类型相同时,elements当然应该就是它自己;

添加延迟处理的行为

看到这,你可能会想,这也没延迟处理什么啊。的确,到目前为止,我们只定义了一个具备接口转发能力的LazySequence以及约束LazySequence访问方式的接口LazySequenceProtocol。因此,我们刚完成在这一节开始提到的前半部分功能:把原始的Sequence保存起来。接下来,我们就得对着Sequence支持的方法,分别实现每一个要“延迟处理”的方法了。这里,我们用map来举例。

Map.swift里,我们可以找到一个extension LazySequenceProtocol

extension LazySequenceProtocol {
  /// Returns a `LazyMapSequence` over this `Sequence`.  The elements of
  /// the result are computed lazily, each time they are read, by
  /// calling `transform` function on a base element.
  @inlinable
  public func map<U>(
    _ transform: @escaping (Elements.Element) -> U
  ) -> LazyMapSequence<Self.Elements, U> {
    return LazyMapSequence(_base: self.elements, transform: transform)
  }
}

因此,当我们调用一个遵从了LazySequenceProtocol对象的map方法,会得到一个LazyMapSequence对象。它的定义在这里

@_fixed_layout
public struct LazyMapSequence<Base : Sequence, Element> {

  public typealias Elements = LazyMapSequence

  @usableFromInline
  internal var _base: Base
  @usableFromInline
  internal let _transform: (Base.Element) -> Element

  /// Creates an instance with elements `transform(x)` for each element
  /// `x` of base.
  @inlinable
  internal init(_base: Base, transform: @escaping (Base.Element) -> Element) {
    self._base = _base
    self._transform = transform
  }
}

在它的定义里:

  • _base表示封装的原始序列;
  • Element是原始序列中的元素变换之后的类型;
  • _transform是暂存的用于变换的方法;

于是,还是上一节提到的Fibonacci序列,当我们使用:

let fibo = Fibonacci()
var mcount = 0

let lazyFibo = fibo.lazy.map {
    (n: Int) -> Int in
    mcount += 1
    print("\(mcount) map")
    return n*2
}

的时候,就不会立即map所有的元素了,而只是会把原始的fibo和要变换的closure都暂时缓存起来。那什么时候才会真正执行变换呢?答案当然是真正访问序列中元素的时候,例如:

var iter = lazyFibo.makeIterator()
for _ in 1...5 { iter.next() }

执行一下,就会在控制台看到这样的结果:

1 map
2 map
3 map
4 map
5 map

也就是说,实际的变换,只发生了5次,而不是eager版本的1000次,而这,就是lazy属性为Fibonacci带来的效果。但是,为什么访问LazyMapSequence就可以按需获取了呢?答案当然在与它搭配工作的Iterator里。

LazyMapSequence.Iterator

它的定义在这里

extension LazyMapSequence {
  @_fixed_layout
  public struct Iterator {
    @usableFromInline
    internal var _base: Base.Iterator
    @usableFromInline
    internal let _transform: (Base.Element) -> Element

    @inlinable
    public var base: Base.Iterator { return _base }

    @inlinable
    internal init(
      _base: Base.Iterator,
      _transform: @escaping (Base.Element) -> Element
    ) {
      self._base = _base
      self._transform = _transform
    }
  }
}

可以看到,都是一些模板代码,没什么实际的东西。而真正的秘密,在这个Iteratornext方法里:

extension LazyMapSequence.Iterator: IteratorProtocol, Sequence {
  @inlinable
  public mutating func next() -> Element? {
    return _base.next().map(_transform)
  }
}

可以看到,“延迟版本”的map,会先从原始序列中取得下一个元素(_base.next()),然后单独对它进行变换。这样,就实现了“按需变换”的效果。

最后,只要让LazyMapSequence遵从LazySequenceProtocol,并定制这种况下的makeIterator方法就好了:

extension LazyMapSequence: LazySequenceProtocol {
  @inlinable
  public func makeIterator() -> Iterator {
    return Iterator(_base: _base.makeIterator(), _transform: _transform)
  }
}

LazyMapSequence遵从了LazySequenceProtocol之后,它就有了一个lazy属性,由于这个属性也是遵从LazySequenceProtocol的,因此,当我们使用lazy.map的时候,就会得到LazyMapSequence对象,当我们调用这个对象的makeIterator,就会是上面定义的这个方法。而这,就是“延迟变换”所有的秘密了。

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

推荐阅读更多精彩内容