1. 概述

在使用 Kotlin 的 List 时,我们经常需要对列表中的每个元素执行某种操作。Kotlin 提供了一组强大的高阶函数,可以高效地对列表元素应用函数。

本文将深入探讨实现这一常见任务的多种方法和技巧。

2. 问题背景

对列表元素应用函数通常涉及两种典型场景:

转换元素(Transformation)
我们希望将原列表中的每个元素通过某个函数映射为新值。例如:

  • Int 列表转为 String 列表(调用 toString()
  • 将 ID 列表通过数据库查询转为对应的实体对象

在这种情况下,每个元素作为函数的输入参数,函数的返回值即为转换后的结果元素

执行动作(Side Effect)
我们只是想对每个元素执行某个操作,但并不关心函数的返回值。例如:

  • 给一批手机号发送短信
  • 将每个元素打印到控制台作为日志输出

本文将分别讨论这两种场景,并介绍对应的解决方案。但在深入之前,先准备两个示例扩展函数:

fun String.reverseCase() = map { c -> if (c.isLowerCase()) c.uppercase() else c.lowercase() }.joinToString(separator = "")

fun String.printCaseReversed() = println("$this -> ${reverseCase()}")

如上所示,我们定义了两个 String 的扩展函数

  • reverseCase():反转字符串中每个字符的大小写并返回新字符串
  • printCaseReversed():内部调用 reverseCase() 并打印结果,无返回值(返回 Unit

接下来初始化测试数据:

val myList = listOf("a a a", "B B B", "c C c", "D d D", "e E E", "F F f")

若对 myList 中每个元素应用 reverseCase(),预期结果应为:

val expected = listOf("A A A", "b b b", "C c C", "d D d", "E e e", "f f F")

为验证正确性,后续示例将使用单元测试断言进行校验。

3. 使用 map() 函数

当涉及列表转换时,map() 是最常用的方法。其核心行为是:

🔁 遍历列表,将每个元素 A 映射为新值 B,最终生成一个包含所有 B 的新列表

下面用 map()myList 应用 reverseCase()

val result = myList.map { it.reverseCase() }
assertEquals(expected, result)

关键点:

  • ✅ 返回一个全新的列表对象(不可变)
  • ❌ 不修改原始列表
  • ⚠️ 原始列表 myList 必须是只读的(List<T>),否则可能引发并发修改问题

这是函数式编程中最推荐的“无副作用”处理方式。

4. 在 MutableList 上使用 replaceAll()

如果目标是就地修改(in-place mutation)列表本身(而非创建新列表),且列表类型为 MutableList,可使用 replaceAll()

val mutableList = myList.toMutableList()
assertEquals(myList, mutableList)

mutableList.replaceAll { it.reverseCase() }
assertEquals(expected, mutableList)

注意要点:

  • ✅ 真正实现了原地替换,内存更友好
  • ✅ 语法与 map() 高度相似,学习成本低
  • ⚠️ 仅适用于 MutableList,对只读 List 调用会抛出异常
  • ⚠️ 破坏了不可变性原则,在多线程或函数式风格代码中需谨慎使用

这个方法适合性能敏感且确定无共享引用的场景。

5. 使用 forEach() 函数

当我们只关心执行动作(如打印、发消息),而不关心返回值时,forEach() 是直观选择:

myList.forEach { it.printCaseReversed() } // 输出每个元素处理结果

输出如下:

a a a -> A A A
B B B -> b b b
c C c -> C c C
D d D -> d D d
e E E -> E e e
F F f -> f f F

也可用于填充另一个集合:

val result = mutableListOf<String>()
myList.forEach { result += it.reverseCase() }
assertEquals(expected, result)

⚠️ 但必须注意:forEach() 的函数签名决定了它的局限性:

public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit

这意味着:

  • ❌ 返回值为 Unit(类似 Java 的 void
  • 无法链式调用后续操作(它是终端操作)
  • ❌ 在流式处理中会中断管道

这在某些复杂逻辑中会带来不便——比如你想先打印日志,再做转换。

6. 使用 onEach() 函数

为解决 forEach() 不能链式调用的问题,Kotlin 提供了 onEach()

public inline fun <T, C : Iterable<T>> C.onEach(action: (T) -> Unit): C {
    return apply { for (element in this) action(element) }
}

对比 forEach()onEach(): | 方法 | 返回类型 | 是否支持链式调用 | |------|--------|----------------| | forEach() | Unit | ❌ | | onEach() | 原集合本身 (C) | ✅ |

示例:

val result = myList.onEach { it.printCaseReversed() }
assertSame(myList, result) // 返回的是原列表

输出与 forEach() 相同:

a a a -> A A A
B B B -> b b b
c C c -> C c C
D d D -> d D d
e E E -> E e e
F F f -> f f F

✅ 最大优势:支持链式操作!

val resultList = myList.onEach { it.printCaseReversed() }
  .map { it.uppercase().replace(" ", ", ") }

assertEquals(listOf("A, A, A", "B, B, B", "C, C, C", "D, D, D", "E, E, E", "F, F, F"), resultList)

💡 踩坑提示:很多人误以为 onEach()forEach() 的替代品,其实它更适合作为“中间调试”工具——比如你在 .filter().map().sorted() 流程中想插入日志,用 onEach() 再合适不过。

7. 总结

场景 推荐方法 特点
转换元素 → 新列表 map() 函数式首选,安全不可变
就地修改可变列表 replaceAll() 内存高效,注意线程安全
执行副作用动作 forEach() 简单直接,但终结流
中间插入副作用 onEach() 支持链式调用,调试利器

选择建议:

  • 默认优先使用 map() + 不可变列表
  • 性能关键且确定上下文安全时用 replaceAll()
  • 日志/监控等中间操作务必用 onEach() 避免破坏流式处理

完整示例代码详见 GitHub 仓库


原始标题:Apply a Function to Each Element of a List in Kotlin