1. 概述

在本篇文章中,我们将深入探讨 Kotlin 中 fold()reduce() 两个高阶函数之间的关键差异。

尽管它们都用于遍历集合并对元素进行累积操作,但其行为和适用场景有显著不同。理解这些差异能帮助你在实际开发中避免踩坑,写出更健壮的代码✅。

2. reduce()

reduce() 的核心作用是:将一个非空集合归约为单一结果值

它通过一个 lambda 表达式,从左到右依次将前一步的“累计值”与当前元素合并,最终返回这个累计值。

基本用法示例

val numbers: List<Int> = listOf(1, 2, 3)
val sum: Int = numbers.reduce { acc, next -> acc + next }
assertEquals(6, sum)

上面这段代码等价于:

  • 第一步:acc = 1, next = 21 + 2 = 3
  • 第二步:acc = 3, next = 33 + 3 = 6

最终返回 6

⚠️ 空集合问题

reduce() 最大的坑就是——不能处理空集合 ❌。

val emptyList = listOf<Int>()
assertThrows<RuntimeException> { 
    emptyList.reduce { acc, next -> acc + next } 
}

此时会抛出 UnsupportedOperationException(运行时异常),因为没有初始值可作为起点。

💡 提示:如果你不确定集合是否为空,直接用 reduce() 就可能线上翻车,务必警惕!

泛型限制:无法改变返回类型

再来看 reduce() 的方法签名:

inline fun <S, T : S> Iterable<T>.reduce(operation: (acc: S, T) -> S): S

注意:T 必须是 S 的子类型。这意味着输入元素类型和累计值类型之间存在继承关系约束。

举个例子:你想把 Int 列表求和,但结果用 Long 接收(防止溢出):

// ❌ 编译失败!Long 不是 Int 的超类
val sum: Long = numbers.reduce<Long, Int> { acc, next -> acc + next.toLong() }

虽然逻辑上合理,但编译器不买账。你只能退而求其次使用 Number 作为中间类型,但这治标不治本。

✅ 结论:reduce() 无法自由变更返回类型,灵活性受限。

3. fold()

相比之下,fold() 更加灵活,因为它允许你指定一个初始值(initial value)

基本用法 & 空集合安全

val sum: Int = numbers.fold(0) { acc, next -> acc + next }
assertEquals(6, sum)

如果集合为空呢?

val emptyList = listOf<Int>()
val sumEmpty = emptyList.fold(0) { acc, next -> acc + next }
assertEquals(0, sumEmpty) // ✅ 正常返回初始值

👉 所以 fold()空集合安全的,这是相比 reduce() 的一大优势。

方法签名:支持任意返回类型

inline fun <T, R> Iterable<T>.fold(initial: R, operation: (acc: R, T) -> R): R

这里 T 是集合元素类型,R 是返回类型,二者完全独立。也就是说,你可以轻松实现类型转换。

示例:Int 列表求和返回 Long

val sum: Long = numbers.fold(0L) { acc, next -> acc + next.toLong() }
assertEquals(6L, sum)

✅ 成功绕开泛型限制,完美解决溢出风险。

高阶用法:构建复杂结构

得益于类型自由,fold() 可以轻松完成数据分类、分组等任务。

比如将数字分为奇偶两组:

val (even, odd) = numbers.fold(
    Pair(mutableListOf<Int>(), mutableListOf<Int>())
) { pair, number ->
    pair.apply {
        when (number % 2) {
            0 -> first.add(number)
            else -> second.add(number)
        }
    }
}

assertEquals(listOf(2), even)
assertEquals(listOf(1, 3), odd)

这种模式在做数据预处理时非常实用,比写多个循环清晰得多。

4. fold 与 reduce 的变体

Kotlin 标准库还提供了一些扩展版本,满足更多场景需求。

从右向左遍历

  • foldRight() / reduceRight():从集合末尾开始处理。
val reversed = numbers.foldRight(listOf<Int>()) { next, acc -> acc + next }
assertEquals(listOf(3, 2, 1), reversed)

⚠️ 注意参数顺序变了!lambda 中是 { next, acc },不是 { acc, next }

带索引的操作

当你需要访问元素下标时,可以使用带 Indexed 的版本:

函数名 说明
foldIndexed() 从左开始,带索引
foldRightIndexed() 从右开始,带索引
reduceIndexed() 左向归约,带索引
reduceRightIndexed() 右向归约,带索引

示例:收集元素索引的逆序

val reversedIndexes = numbers.foldRightIndexed(listOf<Int>()) { i, _, acc -> acc + i }
assertEquals(listOf(2, 1, 0), reversedIndexes)

其中 { i, _, acc } 分别代表:索引、元素(此处忽略)、累计值。

5. 总结

特性 reduce() fold()
是否需要初始值 ❌ 否 ✅ 是
空集合处理 ❌ 抛异常 ✅ 返回初始值
支持类型变换 ❌ 有限制 ✅ 完全自由
使用建议 非空集合、同类型聚合 通用场景、推荐优先使用

最佳实践建议

  • 如果你能100%确保集合非空,且只是简单聚合(如求和、拼接),可用 reduce()
  • 否则一律优先考虑 fold(),它更安全、更灵活,尤其适合业务逻辑复杂的场景。

想进一步了解集合操作?推荐阅读 Kotlin 集合转换指南


原始标题:Difference between fold and reduce in Kotlin