Подтвердить что ты не робот

В 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. Также прояснить некоторые действительно старые ответы, написанные для альфов Котлина, которые не точны для сегодняшнего дня Котлин.

4b9b3361

Ответ 1

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

Во-вторых, для изменения списка "на месте" вам нужно использовать тип списка, который можно изменить. В вашем примере вы используете 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)
}

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

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

Вы можете написать функции расширения для 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)
        }
    }
}

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

Расширенные функции выше немного оптимизированы, если не установить значение, если оно не изменилось на другой экземпляр, и проверка того, что использование === или !== Ссылочное равенство. Не стоит проверять equals() или hashCode(), потому что их вызов имеет неизвестную стоимость, и действительно реляционное равенство ловит любое намерение изменить значение.

Тесты модулей для функций расширения

Ниже приведены примеры unit test работающих функций, а также небольшое сравнение с функцией 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)
    }
}