在 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
),或者使用辅助函数 arrayListOf
或 linkedListOf
来创建 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
扩展函数(它保留了 map
和 mapTo
等这类函数的典型命名):
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() }
这将在迭代时修改列表中的每个项目。
我有一个列表:
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
),或者使用辅助函数 arrayListOf
或 linkedListOf
来创建 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
扩展函数(它保留了 map
和 mapTo
等这类函数的典型命名):
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() }
这将在迭代时修改列表中的每个项目。