В Kotlin, как изменить содержимое списка во время итерации

у меня есть список:

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

и я хочу повторить его при изменении некоторых значений. Я знаю, что могу сделать это с map но это делает копию списка.

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

как это сделать без копии?

Примечание: этот вопрос намеренно написан и на него отвечает автор (Вопросы С Ответами), так что идиоматические ответы на часто задаваемые темы Котлина присутствуют в SO. Также уточните некоторые действительно старые ответы, написанные для Альф Котлина, которые не точны для современного Котлина.

1 ответов


во-первых, не все копирование списка плохо. Иногда копия может воспользоваться кэшем CPU и быть чрезвычайно быстрой, это зависит от списка, размера и других факторов.

во-вторых, для изменения списка "на месте" вам нужно использовать тип списка, который является изменяемым. В вашем примере вы используете listOf возвращает List<T> интерфейс, и это только для чтения. Вам нужно напрямую ссылаться на класс изменяемого списка (т. е. 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)
}

это изменит значения в списке по мере итерации и эффективно для всех типов списков. Чтобы сделать это проще, создайте полезные функции расширения, которые можно использовать повторно (см. ниже).

мутирование с помощью простой функции расширения:

вы можете написать расширение функции для Котлина, которые делают на месте изменяемую итерацию для любого 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)
        }
    }
}

об оптимизации с использованием только ссылочного равенства

функции расширения выше оптимизируют немного, не устанавливая значение, если оно не изменилось на другой экземпляр, проверяя, что с помощью === или !== is Равенство Ссылок. Это не стоит проверять 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)
    }
}