Swift源码阅读 - Sequence变换方法的实现

这一节,我们来看和生成Sequence有关的API。它可以分成两大类:一类是基于原有Sequence中的元素生成各种新的值;另一类,则是分割或合并生成新的Sequence

我们先来看第一类。

变换Sequence相关的API

map

提起变换一个Sequence中的所有元素,你最先想到的应该就是map。那我们就从这个API的实现说起。map的定义在这里,并且Sequence为它提供了一份默认实现:

extension Sequence {
  @inlinable
  public func map<T>(
    _ transform: (Element) throws -> T
  ) rethrows -> [T] {
    let initialCapacity = underestimatedCount
    var result = ContiguousArray<T>()
    result.reserveCapacity(initialCapacity)

    var iterator = self.makeIterator()

    // Add elements up to the initial capacity without checking for regrowth.
    for _ in 0..<initialCapacity {
      result.append(try transform(iterator.next()!))
    }
    // Add remaining elements, if any.
    while let element = iterator.next() {
      result.append(try transform(element))
    }
    return Array(result)
  }
}

逻辑很简单,变换的过程分成三个部分:

  • 首先,根据序列中的underestimatedCount和变换的目标类型T,开辟了一块连续的内存空间,关于这个ContiguousArray,等我们分析到Swift Array的时候还会详细讲到,这里暂时就先把它理解为是内存连的一块空间就好了;
  • 其次,通过iterator遍历序列中的每一个元素,对它进行变换,并把变换后的结果保存在第一步开辟的临时空间里;
  • 最后,用这个临时空间生成Array返回;

因此,经过map变换的Sequence就不再是一个简单的序列了,而是一个Array。我们只能对有限序列使用map进行变换。

flatMap

接下来,我们来看经常把新手搞晕的flatMap,通过它的源代码,我们还能看到一些Swift在API更新过程中用到的方法。实际上,在Sequence里,根据变换生成的结果,有两个版本的flatMap

第一个,是单纯的把一个二维数组,变成一个一维数组的。它的定义在这里

extension Sequence {
  @inlinable
  public func flatMap<SegmentOfResult : Sequence>(
    _ transform: (Element) throws -> SegmentOfResult
  ) rethrows -> [SegmentOfResult.Element] {
    var result: [SegmentOfResult.Element] = []
    for element in self {
      result.append(contentsOf: try transform(element))
    }
    return result
  }
}

这里,要注意它的transform参数,这个变换返回的是SegmentOfResult,也就是一个遵从Sequence的类型,对于这种情况:

  • 首先,用transform(element)对之前Sequence中的成员进行变换,每一次变换,都会得到一个新的序列;
  • 其次,用append(contentsOf)把上一步变换得到的序列中的每一个元素添加到生成的一个临时变量里;
  • 最后,把生成的结果返回;

这样,本该返回一个“数组的数组”的结果,就被flatMap变成了一个一维数组。

第二个版本的flatMap,有两个明显的特征:一个是它的transform并不返回集合类型;另一个是它返回的是个Optional,也就是变换是有可能失败的。并且,这个版本的flatMap在Swift 4.1中被标记为过期方法了。它的定义在这里。我们来看看Swift是怎么做的:

extension Sequence {
  @inline(__always)
  @available(swift, deprecated: 4.1, renamed: "compactMap(_:)",
    message: "Please use compactMap(_:) for the case where closure returns an optional value")
  public func flatMap<ElementOfResult>(
    _ transform: (Element) throws -> ElementOfResult?
  ) rethrows -> [ElementOfResult] {
    return try _compactMap(transform)
  }
}

首先,@inine(__always)告诉编译器总是以内联的方式处理这个flatMap,不过这个属性已经被我们之前见过的@inlinable替代掉了,所以我们不用太在意这个。

其次,Swift使用了@available这样的形式来为开发者提供API过期提示:

@available(swift,
  deprecated: 4.1,
  renamed: "compactMap(_:)",
  message: "Please use compactMap(_:) for the case where closure returns an optional value")

其中,

  • deprecated表示开始标记为过期API的版本;
  • rename用于告知开发者新版本API的名称;
  • message则是显示在IDE上的具体提示消息;

如果我们自己也在维护一个程序库,不妨在版本更新的时候,借鉴Swift官方的做法。

第三,就是这个flatMap的实现了,可以看到,它只是把调用转发给了一个叫做_compactMap的方法。

接下来,在跟到这个_compactMap之前,我们先来看一下这个新版本的compactMap,它的定义在这里

extension Sequence {
  @inlinable
  public func compactMap<ElementOfResult>(
    _ transform: (Element) throws -> ElementOfResult?
  ) rethrows -> [ElementOfResult] {
    return try _compactMap(transform)
  }
}

可以看到,它的编译器修饰已经变成了@inlineable。并且,它也把调用转发给了这个_compactMap方法。那么,我们就跟到这个方法里来看看,它的定义在这里

extension Sequence {
  @inlinable // FIXME(sil-serialize-all)
  @inline(__always)
  public func _compactMap<ElementOfResult>(
    _ transform: (Element) throws -> ElementOfResult?
  ) rethrows -> [ElementOfResult] {
    var result: [ElementOfResult] = []
    for element in self {
      if let newElement = try transform(element) {
        result.append(newElement)
      }
    }
    return result
  }
}

可以看到,其实本质上,和我们之前看到的flatMap实现是类似的,只不过,在把变换结果添加到result之前,使用if let进行了nil检查而已。于是,经过compactMap之后,新的数组中元素的个数,可能比原始的Sequence少一些,那些转换失败的元素,都被过滤掉了。而这,也就是compactMap这个名字的由来。

reduce

另一个我们熟悉的变换API是reduce,它把一个序列的所有元素变成一个某种形式的值(当然这个值也可以是一个新的集合)。但是,你知道么?reduce也有两个版本。

第一个版本,就是我们经常用的reduce,它的定义在这里

extension Sequence {
  @inlinable
  public func reduce<Result>(
    _ initialResult: Result,
    _ nextPartialResult:
      (_ partialResult: Result, Element) throws -> Result
  ) rethrows -> Result {
    var accumulator = initialResult
    for element in self {
      accumulator = try nextPartialResult(accumulator, element)
    }
    return accumulator
  }
}

可以看到,没什么好说的,就是不断用nextPartialResult的返回值更新accumulator。等self遍历完之后,把accumulator返回就好了。但是,当我们需要让reduce返回一个集合类型的时候,这个实现就显得不那么高效了。因为此时,nextPartialResult返回的就是一个集合类型,每一次遍历,就会导致一次集合的拷贝,集合中的元素越多,拷贝就可能越耗时。

来看一个Swift官方提供的例子:

let letters = "abracadabra"

let letterCount = letters.reduce([:]) {
  (counts: [Character: Int], letter:Character) in
  var temp = counts
  temp[letter, default: 0] += 1

  return temp
}

// ["b": 2, "a": 5, "r": 2, "d": 1, "c": 1]

为了统计letters中每个字符出现的次数,在reduce的closure中,由于counts是只读的,我们每一次计算完迭代到的letter出现的次数之后,都要拷贝并返回一个新的Dictionary,显然这并不是我们期望的。

为此,Swift提供了另一个版本的reduce,它的定义在这里

extension Sequence {
  @inlinable
  public func reduce<Result>(
    into initialResult: Result,
    _ updateAccumulatingResult:
      (_ partialResult: inout Result, Element) throws -> ()
  ) rethrows -> Result {
    var accumulator = initialResult
    for element in self {
      try updateAccumulatingResult(&accumulator, element)
    }
    return accumulator
  }
}

可以看到,这次partialResult的第一个参数,添加了inout修饰,updateAccumulatingResult可以直接通过这个参数记录多次迭代的结果,这样就避免了反复返回对象的问题。可以看到updateAccumulatingResult是没有返回值的。因此,在这个版本reduce的现实里,我们是try updateAccumulatingResult(&accumulator, element)来实现每一次合并的。

有了这个版本的reduce之后,刚才的例子就可以写成这样:

let letterCount = letters.reduce(into: [:]) {
  counts, letter in
  counts[letter, default: 0] += 1
}

可以看到,无论是代码本身,还是执行效率,都比之前的版本好多了。

Eager methods

以上,就是和Sequence变换有关的API,其实,这些API有一个共性,Swift官方管它们叫做eager algorithm,什么意思呢?来看个例子:

struct Fibonacci: Sequence {
    typealias Element = Int
    func makeIterator() -> FiboIter {
        return FiboIter()
    }
}

struct FiboIter: IteratorProtocol {
    var state = (0, 1)

    mutating func next() -> Int? {
        let nextNumber = state.0
        self.state = (state.1, state.0 + state.1)

        if nextNumber <= 1000 {
            return nextNumber
        }

        return nil
    }
}

这是个包含所有小于1000的Fibnacci数列,但它的对象并不会占用内存空间。如果我们把它map之后,就会变成一个17个Intarray,这种根据序列中所有元素个数“野蛮”占据内存空间的行为,就叫做eager。这时,你可能会想,既然序列本身的元素可以“按需获取”,为什么map之后不行呢?

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容