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 = 2
→1 + 2 = 3
- 第二步:
acc = 3
,next = 3
→3 + 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 集合转换指南。