TL;博士
当仅涉及展平二维数组时(未应用任何转换或分隔符,请参阅 @dfri's answer 了解有关该方面的更多信息),array.flatMap{$0} 和 Array(array.joined()) 在概念上是相同的并且具有相似的性能。
flatMap(_:) 和joined() 之间的主要区别(注意这不是一个新方法,它有just been renamed from flatten())是joined() 总是延迟应用(对于数组,它返回一个特殊的@987654323 @)。
因此,就性能而言,在您只想迭代平展序列的一部分(不应用任何转换)的情况下,使用 joined() 而不是 flatMap(_:) 是有意义的。例如:
let array2D = [[2, 3], [8, 10], [9, 5], [4, 8]]
if array2D.joined().contains(8) {
print("contains 8")
} else {
print("doesn't contain 8")
}
因为joined() 是延迟应用的,而contains(_:) 将在找到匹配项时停止迭代,所以只有前两个内部数组必须“展平”才能从二维数组中找到元素8。虽然,作为@dfri correctly notes below,您也可以通过使用LazySequence/LazyCollection 懒惰地应用flatMap(_:)——可以通过lazy 属性创建。这对于懒惰地应用变换和展平给定的 2D 序列是理想的。
在完全迭代joined() 的情况下,它在概念上与使用flatMap{$0} 没有区别。因此,这些都是展平二维数组的有效(并且概念上相同)的方法:
array2D.joined().map{$0}
Array(array2D.joined())
array2D.flatMap{$0}
就性能而言,flatMap(_:) is documented 的时间复杂度为:
O(m + n),其中m是这个序列的长度,n是结果的长度
这是因为its implementation 很简单:
public func flatMap<SegmentOfResult : Sequence>(
_ transform: (${GElement}) throws -> SegmentOfResult
) rethrows -> [SegmentOfResult.${GElement}] {
var result: [SegmentOfResult.${GElement}] = []
for element in self {
result.append(contentsOf: try transform(element))
}
return result
}
}
由于append(contentsOf:) 的时间复杂度为 O(n),其中 n 是要追加的序列的长度,我们得到 O(m + n) 的整体时间复杂度,其中 m 将是总长度附加所有序列,n是二维序列的长度。
对于joined(),没有记录时间复杂性,因为它是延迟应用的。但是,要考虑的主要源代码位是 the implementation of FlattenIterator,它用于迭代 2D 序列的展平内容(这将在使用 map(_:) 或 Array(_:) 初始化程序与 joined() 时发生)。
public mutating func next() -> Base.Element.Iterator.Element? {
repeat {
if _fastPath(_inner != nil) {
let ret = _inner!.next()
if _fastPath(ret != nil) {
return ret
}
}
let s = _base.next()
if _slowPath(s == nil) {
return nil
}
_inner = s!.makeIterator()
}
while true
}
这里_base 是基本二维序列,_inner 是来自内部序列之一的当前迭代器,_fastPath 和 _slowPath 是对编译器的提示,以帮助进行分支预测。
假设我正确地解释了这段代码并迭代了整个序列,这也有 O(m + n) 的时间复杂度,其中 m 是序列的长度,n 是结果的长度.这是因为它通过每个外部迭代器和每个内部迭代器来获取展平的元素。
因此,就性能而言,Array(array.joined()) 和 array.flatMap{$0} 都具有相同的时间复杂度。
如果我们在调试版本 (Swift 3.1) 中运行快速基准测试:
import QuartzCore
func benchmark(repeatCount:Int = 1, name:String? = nil, closure:() -> ()) {
let d = CACurrentMediaTime()
for _ in 0..<repeatCount {
closure()
}
let d1 = CACurrentMediaTime()-d
print("Benchmark of \(name ?? "closure") took \(d1) seconds")
}
let arr = [[Int]](repeating: [Int](repeating: 0, count: 1000), count: 1000)
benchmark {
_ = arr.flatMap{$0} // 0.00744s
}
benchmark {
_ = Array(arr.joined()) // 0.525s
}
benchmark {
_ = arr.joined().map{$0} // 1.421s
}
flatMap(_:) 似乎是最快的。我怀疑joined() 变慢可能是由于FlattenIterator 中发生的分支(尽管编译器的提示最小化了这个成本)——尽管为什么map(_:) 这么慢,我不太确定。肯定有兴趣知道其他人是否对此了解更多。
但是,在优化的构建中,编译器能够优化掉这个巨大的性能差异;为所有三个选项提供相当的速度,尽管flatMap(_:) 仍然是最快的几分之一秒:
let arr = [[Int]](repeating: [Int](repeating: 0, count: 10000), count: 1000)
benchmark {
let result = arr.flatMap{$0} // 0.0910s
print(result.count)
}
benchmark {
let result = Array(arr.joined()) // 0.118s
print(result.count)
}
benchmark {
let result = arr.joined().map{$0} // 0.149s
print(result.count)
}
(请注意,执行测试的顺序会影响结果 - 以上两个结果都是以各种不同顺序执行测试的平均值)