这一节,我们来看和生成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个Int
的array
,这种根据序列中所有元素个数“野蛮”占据内存空间的行为,就叫做eager。这时,你可能会想,既然序列本身的元素可以“按需获取”,为什么map
之后不行呢?