1. 简介

在遍历一个 List 的时候,有时我们不仅需要当前元素,还需要访问前一个元素。比如,我们有一个列表 [1, 2, 3, 4, 5],我们希望依次处理:只看 1,然后看 1 和 2,接着是 2 和 3,以此类推。最终可能想生成一个新列表,包含所有相邻元素的组合,如:["1", "12", "23", "34", "45"]

在本教程中,我们将介绍几种 Kotlin 中实现这种需求的方式,包括使用传统循环、foldIndexed()zipWithNext()scan() 方法等。这些方法都适用于需要在遍历中引用前一个元素的场景。


2. 使用 for 循环

这是最直接的方法,通过索引访问前一个元素:

fun iterateListUsingLoop(list: List<Int>): List<String> {
    val newList = mutableListOf<String>()
    newList.add(list[0].toString())
    for (i in 1 until list.size) {
        newList.add("${list[i - 1]}${list[i]}")
    }
    return newList
}

说明:

  • 创建一个 MutableList 存储结果;
  • 第一个元素单独处理;
  • 从索引 1 开始遍历,每次访问当前元素和前一个元素;
  • 将它们拼接后加入新列表。

测试用例:

@Test
fun `creates new list by adding elements from original list using loop`() {
    val list = listOf(1, 2, 3, 4, 5)
    val expectedList = listOf("1", "12", "23", "34", "45")

    assertEquals(expectedList, iterateListUsingLoop(list))
}

优点: 逻辑清晰,适合初学者理解和使用。

⚠️ 缺点: 需要手动处理索引边界。


3. 使用 foldIndexed 方法

foldIndexed() 是 Kotlin 标准库中的高阶函数,适合在遍历过程中累积结果,同时可以访问索引:

fun iterateListUsingFoldIndexed(list: List<Int>): List<String> {
    return list.foldIndexed(mutableListOf()) { i, acc, element ->
        if(i == 0) {
            acc.add(element.toString())
        } else {
            acc.add("${list[i - 1]}$element")
        }
        acc
    }
}

说明:

  • 使用 foldIndexed 构建一个累加器(accumulator);
  • 索引为 0 时,仅添加当前元素;
  • 索引大于 0 时,拼接当前元素与前一个元素;
  • 最终返回整个结果列表。

测试用例:

@Test
fun `creates new list by adding elements from original list using fold method`() {
    val list = listOf(1, 2, 3, 4, 5)
    val expectedList = listOf("1", "12", "23", "34", "45")

    assertEquals(expectedList, iterateListUsingFoldIndexed(list))
}

优点: 函数式风格,代码简洁。

⚠️ 缺点: 对不熟悉高阶函数的开发者来说可读性略差。


4. 使用 zipWithNext 方法

zipWithNext() 是 Kotlin 标准库中用于将列表中相邻元素配对的方法,非常适合此类场景:

@Test
fun `creates new list by adding elements from original list using zipWithNext method`() {
    val list = listOf("1", "2", "3", "4", "5")
    val expectedList = listOf("1", "12", "23", "34", "45")
    val result = (list.take(1) + list.zipWithNext { a, b -> "$a$b" })

    assertEquals(expectedList, result)
}

说明:

  • take(1) 用于保留第一个元素;
  • zipWithNext 会生成 (a, b) 对,其中 a 是当前元素,b 是下一个元素;
  • 然后将这两个元素拼接起来;
  • 最终将第一个元素与拼接结果合并成一个完整列表。

优点: 非常直观,代码简短。

⚠️ 缺点: 不适合需要访问前一个元素的复杂逻辑。


5. 使用 scan 方法

scan 是 Kotlin 1.4+ 引入的新方法,非常适合需要逐步累积状态的场景:

@Test
fun `creates new list by adding elements from original list using scan method`() {
    val list = listOf(1, 2, 3, 4, 5)
    val expectedList = listOf("1", "12", "23", "34", "45")
    val result = list.drop(1).scan(list.first().toString()) { acc, i -> acc.takeLast(1) + i.toString() }

    assertEquals(expectedList, result)
}

说明:

  • drop(1) 用于跳过第一个元素;
  • 初始值为第一个元素的字符串;
  • 每次取前一次结果的最后一个字符,与当前元素拼接;
  • 逐步构建出最终的字符串列表。

优点: 适合需要累积状态的复杂逻辑。

⚠️ 缺点: 理解门槛稍高,不适合新手。


6. 总结

方法 适用场景 优点 缺点
for 循环 通用、直观 简单易懂 需要手动处理索引边界
foldIndexed() 函数式编程风格 代码简洁 对新手不太友好
zipWithNext() 遍历相邻元素 非常简洁 无法处理复杂逻辑
scan() 状态累积型逻辑 灵活强大 理解成本较高

建议:

  • 如果逻辑简单,优先使用 forzipWithNext()
  • 如果需要函数式风格,使用 foldIndexed()
  • 如果需要维护状态或处理更复杂的累积逻辑,使用 scan()

在实际开发中,选择哪种方式取决于项目风格和团队熟悉度。合理使用这些技巧,可以写出更简洁、更易维护的 Kotlin 代码。


原始标题:Iterate a Collection Referencing Previous Element in Kotlin