在 Kotlin 中,如何在迭代时修改列表的内容

In Kotlin, how do you modify the contents of a list while iterating

我有一个列表:

val someList = listOf(1, 20, 10, 55, 30, 22, 11, 0, 99)

而且我想在修改一些值的同时迭代它。我知道我可以使用 map 来完成,但这会生成列表的副本。

val copyOfList = someList.map { if (it <= 20) it + 20 else it }

没有副本怎么办?

注意: 这个问题是作者(Self-Answered Questions)特意写下并回答的,以便对常见的 Kotlin 主题进行地道的回答存在于 SO 中。还要澄清一些为 Kotlin alpha 编写的非常古老的答案,这些答案对于当今的 Kotlin 来说并不准确。

首先,并非所有的列表复制都是不好的。有时副本可以利用 CPU 缓存并且非常快,这取决于列表、大小和其他因素。

其次,要修改列表 "in-place",您需要使用一种可变的列表。在您的示例中,您使用 listOf,其中 returns 是 List<T> 接口,即 read-only。您需要直接引用可变列表的 class(即 ArrayList),或者使用辅助函数 arrayListOflinkedListOf 来创建 MutableList<T>参考。一旦你有了它,你可以使用 listIterator() 迭代列表,它有一个变异方法 set().

// create a mutable list
val someList = arrayListOf(1, 20, 10, 55, 30, 22, 11, 0, 99)

// iterate it using a mutable iterator and modify values 
val iterate = someList.listIterator()
while (iterate.hasNext()) {
    val oldValue = iterate.next()
    if (oldValue <= 20) iterate.set(oldValue + 20)
}

这将在迭代发生时更改列表中的值,并且对所有列表类型都有效。为了使这更容易,创建有用的扩展函数,您可以 re-use(见下文)。

使用简单的扩展函数进行变异:

您可以为 Kotlin 编写扩展函数,为任何 MutableList 实现执行就地可变迭代。这些内联函数的执行速度与迭代器的任何自定义使用一样快,并且内联以提高性能。非常适合 Android 或任何地方。

这是一个 mapInPlace 扩展函数(它保留了 mapmapTo 等这类函数的典型命名):

inline fun <T> MutableList<T>.mapInPlace(mutator: (T)->T) {
    val iterate = this.listIterator()
    while (iterate.hasNext()) {
        val oldValue = iterate.next()
        val newValue = mutator(oldValue)
        if (newValue !== oldValue) {
            iterate.set(newValue)
        }
    }
}

示例 调用此扩展函数的任何变体:

val someList = arrayListOf(1, 20, 10, 55, 30, 22, 11, 0, 99)
someList.mapInPlace { if (it <= 20) it + 20 else it }

这不是对所有 Collection<T> 通用的,因为大多数迭代器只有 remove() 方法,而不是 set().

数组的扩展函数

您可以使用类似的方法处理泛型数组:

inline fun <T> Array<T>.mapInPlace(mutator: (T)->T) {
    this.forEachIndexed { idx, value ->
        mutator(value).let { newValue ->
            if (newValue !== value) this[idx] = mutator(value)
        }
    }
}

并且对于每个原始数组,使用以下变体:

inline fun BooleanArray.mapInPlace(mutator: (Boolean)->Boolean) {
    this.forEachIndexed { idx, value ->
        mutator(value).let { newValue ->
            if (newValue !== value) this[idx] = mutator(value)
        }
    }
}

关于仅使用参考平等的优化

上面的扩展函数通过不设置值来优化一点,如果它没有更改为不同的实例,检查使用 ===!==Referential Equality。不值得检查 equals()hashCode(),因为调用它们的成本未知,而且实际上引用相等会捕获任何更改值的意图。

扩展函数的单元测试

这里是单元测试用例,显示了函数的工作情况,以及与复制的 stdlib 函数 map() 的一个小比较:

class MapInPlaceTests {
    @Test fun testMutationIterationOfList() {
        val unhappy = setOf("Sad", "Angry")
        val startingList = listOf("Happy", "Sad", "Angry", "Love")
        val expectedResults = listOf("Happy", "Love", "Love", "Love")

        // modify existing list with custom extension function
        val mutableList = startingList.toArrayList()
        mutableList.mapInPlace { if (it in unhappy) "Love" else it }
        assertEquals(expectedResults, mutableList)
    }

    @Test fun testMutationIterationOfArrays() {
        val otherArray = arrayOf(true, false, false, false, true)
        otherArray.mapInPlace { true }
        assertEquals(arrayOf(true, true, true, true, true).toList(), otherArray.toList())
    }

    @Test fun testMutationIterationOfPrimitiveArrays() {
        val primArray = booleanArrayOf(true, false, false, false, true)
        primArray.mapInPlace { true }
        assertEquals(booleanArrayOf(true, true, true, true, true).toList(), primArray.toList())
    }

    @Test fun testMutationIterationOfListWithPrimitives() {
        val otherList = arrayListOf(true, false, false, false, true)
        otherList.mapInPlace { true }
        assertEquals(listOf(true, true, true, true, true), otherList)
    }
}

这是我想出的方法,与 Jayson 的方法类似:

inline fun <T> MutableList<T>.mutate(transform: (T) -> T): MutableList<T> {
    return mutateIndexed { _, t -> transform(t) }
}

inline fun <T> MutableList<T>.mutateIndexed(transform: (Int, T) -> T): MutableList<T> {
    val iterator = listIterator()
    var i = 0
    while (iterator.hasNext()) {
        iterator.set(transform(i++, iterator.next()))
    }
    return this
}

无需编写任何新的扩展方法 - 是的,功能范式很棒,但它们 do 通常意味着不变性。如果你正在变异,你可能会考虑通过老派来隐含它:

    val someList = mutableListOf(1, 20, 10, 55, 30, 22, 11, 0, 99)

    for(i in someList.indices) {
        val value = someList[i]
        someList[i] = if (value <= 20) value + 20 else value
    }

您可以使用 list.forEach { item -> item.modify() }

这将在迭代时修改列表中的每个项目。