Swift 中怎样更快地 reduce

3,955 阅读3分钟

在 Swift 中,对于集合类型,Swift 标准库提供了若干方便的方法,可以对数据进行处理,其中一个比较常见的就是 reduce。reduce 这个单词,通过查阅字典,可以发现其有“简化、归纳”的意思,也就是说,可以用 reduce 把一组数据归纳为一个数据,当然这个一个数据也可以是一个数组或任何类型。

比较常见的 reduce 使用案例,例如:

求和:

let numbers = [1, 2, 3, 4, 5]
let sum = numbers.reduce(0, +)
print(sum) // 输出 15

字符串拼接:

let words = ["hello", "world", "how", "are", "you"]
let sentence = words.reduce("", { $0 + " " + $1 })
print(sentence) // 输出 " hello world how are you"

两个 reduce API

观察 reduce 方法的声明,会发现有两个不同的 API,一个是 reduce 一个是 reduce(into:),他们的功能是一样的,但是却略有不同。

reduce 方法的函数签名如下:

func reduce<Result>(_ initialResult: Result, _ nextPartialResult: (Result, Element) throws -> Result) rethrows -> Result

该方法接收一个初始值和一个闭包作为参数,该闭包将当前的结果值和集合中的下一个元素作为输入,并返回一个新的结果值。reduce 方法依次迭代集合中的每个元素,并根据闭包的返回值更新结果值,最终返回最终结果值。

还是回到最简单的求和上来,下面的代码使用 reduce 方法计算一个数组中所有元素的总和:

let numbers = [1, 2, 3, 4, 5]
let sum = numbers.reduce(0, { $0 + $1 })
print(sum) // 输出 15

reduce(into:) 方法的函数签名如下:

func reduce<Result>( into initialResult: __owned Result, _ updateAccumulatingResult: (inout Result, Element) throws -> Void ) rethrows -> Result

该方法接收一个初始值和一个闭包作为参数,该闭包将当前的结果值和集合中的下一个元素作为输入,并使用 inout 参数将更新后的结果值传递回去。reduce(into:) 方法依次迭代集合中的每个元素,并根据闭包的返回值更新结果值,最终返回最终结果值。

下面的代码使用 reduce(into:) 方法计算一个数组中所有元素的总和:

let numbers = [1, 2, 3, 4, 5]
let sum = numbers.reduce(into: 0, { result, element in
    result += element
})
print(sum) // 输出 15

可以看到,reduce(into:) 方法中闭包的参数使用了 inout 关键字,使得闭包内部可以直接修改结果值。这样可以避免不必要的内存分配和拷贝,因此在处理大量数据时,使用 reduce(into:) 方法可以提高性能。

观察源码

我们再通过观察源码证实这一结论

reduce 方法的源码实现如下:

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

可以发现这里有两处拷贝,一处是在 accumulator 传参给 nextPartialResult 时,一处是在把 nextPartialResult 的结果赋值给 accumulator 变量时,由于这里的 accumulator 的类型是一个值类型,每次赋值都会触发 Copy-on-Write 中的真正的拷贝。并且这两处拷贝都是在循环体中,如果循环的次数非常多,是会大大拖慢性能的。

再看 reduce(into:) 方法的源码:

func reduce<Result>( 
    into initialResult: __owned Result, 
    _ updateAccumulatingResult: (inout Result, Element) throws -> Void 
) rethrows -> Result { 
    var result = initialResult 
    for element in self { 
        try updateAccumulatingResult(&result, element) 
    } 
    return result 
}

在方法的实现中,我们首先将 initialResult 复制到一个可变变量 result 中。然后,我们对序列中的每个元素调用 updateAccumulatingResult 闭包,使用 & 语法将 result 作为 inout 参数传递给该闭包。因此这里每次循环都是在原地修改 result 的值,并没有发生拷贝操作。

总结

因此,reduce 方法和 reduce(into:) 方法都可以用来将集合中的元素组合成单个值,但是对于会触发 Copy-on-Write 的类型来说, reduce(into:) 方法可以提供更好的性能。在实际使用中,应该根据具体情况选择合适的方法。