web-dev-qa-db-fra.com

Dans Kotlin, comment modifier le contenu d'une liste tout en itérant

J'ai une liste:

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

Et je veux l'itérer tout en modifiant certaines des valeurs. Je sais que je peux le faire avec map mais cela fait une copie de la liste.

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

Comment faire cela sans copie?

Remarque: cette question est intentionnellement écrite et répondue par l'auteur ( questions en réponse ), de sorte que les réponses idiomatiques aux sujets Kotlin fréquemment posés sont présentes dans SO. Aussi pour clarifier certaines réponses vraiment anciennes écrites pour les alphas de Kotlin qui ne sont pas exactes pour le Kotlin actuel.

28
Jayson Minard

Tout d'abord, toutes les copies d'une liste ne sont pas mauvaises. Parfois, une copie peut tirer parti du cache du processeur et être extrêmement rapide, cela dépend de la liste, de la taille et d'autres facteurs.

Deuxièmement, pour modifier une liste "sur place", vous devez utiliser un type de liste qui est modifiable. Dans votre exemple, vous utilisez listOf qui renvoie l'interface List<T>, Et qui est en lecture seule. Vous devez référencer directement la classe d'une liste mutable (c'est-à-dire ArrayList), ou il est idiomatique Kotlin d'utiliser les fonctions d'assistance arrayListOf ou linkedListOf pour créer un MutableList<T> Référence. Une fois que vous avez cela, vous pouvez parcourir la liste en utilisant la listIterator() qui a une méthode de mutation 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)
}

Cela modifiera les valeurs de la liste à mesure que l'itération se produit et est efficace pour tous les types de liste. Pour faciliter cela, créez des fonctions d'extension utiles que vous pouvez réutiliser (voir ci-dessous).

Mutation à l'aide d'une simple fonction d'extension:

Vous pouvez écrire des fonctions d'extension pour Kotlin qui effectuent une itération mutable sur place pour toute implémentation MutableList. Ces fonctions en ligne fonctionneront aussi rapidement que toute utilisation personnalisée de l'itérateur et sont intégrées pour les performances. Parfait pour Android ou n'importe où.

Voici une fonction d'extension mapInPlace (qui conserve la dénomination typique pour ces types de fonctions telles que map et 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)
        }
    }
}

Exemple appelant toute variation de cette fonction d'extension:

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

Ceci n'est pas généralisé pour tous Collection<T>, Car la plupart des itérateurs n'ont qu'une méthode remove(), pas set().

Fonctions d'extension pour les tableaux

Vous pouvez gérer des tableaux génériques avec une méthode similaire:

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

Et pour chacun des tableaux primitifs, utilisez une variation de:

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

A propos de l'optimisation utilisant uniquement l'égalité de référence

Les fonctions d'extension ci-dessus optimisent un peu en ne définissant pas la valeur si elle n'a pas changé pour une instance différente, en vérifiant que l'utilisation de === Ou !== Est égalité référentielle . Cela ne vaut pas la peine de vérifier equals() ou hashCode() car les appeler a un coût inconnu, et vraiment l'égalité référentielle intercepte toute intention de changer la valeur.

Tests unitaires pour les fonctions d'extension

Voici des cas de tests unitaires montrant les fonctions qui fonctionnent, ainsi qu'une petite comparaison avec la fonction stdlib map() qui fait une copie:

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)
    }
}
56
Jayson Minard