美文网首页
Swift源码阅读 - Sequence变换方法的实现

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

作者: 醉看红尘这场梦 | 来源:发表于2020-03-15 18:03 被阅读0次

    这一节,我们来看和生成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之后不行呢?

    相关文章

      网友评论

          本文标题:Swift源码阅读 - Sequence变换方法的实现

          本文链接:https://www.haomeiwen.com/subject/osjyjhtx.html