Kotlin débutant ici. Comment prendre une liste et sans la transformer, créer une deuxième liste (immuable) avec un élément mis à jour à un index spécifique?
Je pense à deux manières, qui semblent toutes deux impliquer des succès en performance, une mutation de l'objet sous-jacent, ou les deux.
data class Player(val name: String, val score: Int = 0)
val players: List<Player> = ...
// Do I do this?
val updatedPlayers1 = players.mapIndexed { i, player ->
if (i == 2) player.copy(score = 100)
else player
}
// Or this?
val updatedPlayer = players[2].copy(score = 100)
val mutable = players.toMutableList()
mutable.set(2, updatedPlayer)
val updatedPlayers2 = mutable.toList()
S'il n'existe aucun moyen performant de le faire, existe-t-il une structure de données plus appropriée dans Kotlin stdlib ou une autre bibliothèque? Kotlin ne semble pas avoir de vecteurs.
Pour moi, évident que cette deuxième voie devrait être plus rapide, mais combien?
J'ai donc écrit quelques repères ici
@State(Scope.Thread)
open class ModifyingImmutableList {
@Param("10", "100", "10000", "1000000")
var size: Int = 0
lateinit var players: List<Player>
@Setup
fun setup() {
players = generatePlayers(size)
}
@Benchmark fun iterative(): List<Player> {
return players.mapIndexed { i, player ->
if (i == 2) player.copy(score = 100)
else player
}
}
@Benchmark fun toMutable(): List<Player> {
val updatedPlayer = players[2].copy(score = 100)
val mutable = players.toMutableList()
mutable.set(2, updatedPlayer)
return mutable.toList()
}
@Benchmark fun toArrayList(): List<Player> {
val updatedPlayer = players[2].copy(score = 100)
return players.set(2, updatedPlayer)
}
}
Et obtenu suivant résultats :
$ Java -jar target/benchmarks.jar -f 5 -wi 5 ModifyingImmutableList
Benchmark (size) Mode Cnt Score Error Units
ModifyingImmutableList.iterative 10 thrpt 100 6885018.769 ± 189148.764 ops/s
ModifyingImmutableList.iterative 100 thrpt 100 877403.066 ± 20792.117 ops/s
ModifyingImmutableList.iterative 10000 thrpt 100 10456.272 ± 382.177 ops/s
ModifyingImmutableList.iterative 1000000 thrpt 100 108.167 ± 3.506 ops/s
ModifyingImmutableList.toArrayList 10 thrpt 100 33278431.127 ± 560577.516 ops/s
ModifyingImmutableList.toArrayList 100 thrpt 100 11009646.095 ± 180549.177 ops/s
ModifyingImmutableList.toArrayList 10000 thrpt 100 129167.033 ± 2532.945 ops/s
ModifyingImmutableList.toArrayList 1000000 thrpt 100 528.502 ± 16.451 ops/s
ModifyingImmutableList.toMutable 10 thrpt 100 19679357.039 ± 338925.701 ops/s
ModifyingImmutableList.toMutable 100 thrpt 100 5504388.388 ± 102757.671 ops/s
ModifyingImmutableList.toMutable 10000 thrpt 100 62809.131 ± 1070.111 ops/s
ModifyingImmutableList.toMutable 1000000 thrpt 100 258.013 ± 8.076 ops/s
Donc, ces tests montrent que parcourir plus de collection environ 3 à 6 fois plus lentement que la copie. Aussi, je fournis mon implémentation: toArray , qui ressemble à plus performant.
Sur 10 éléments, la méthode toArray
a un débit 33278431.127 ± 560577.516
opérations par seconde. Est-ce lent? Ou c'est extrêmement rapide? J'écris le test "de base", qui montre le coût de la copie de Players
et du tableau en mutation. Résultats intéressants:
@Benchmark fun baseline(): List<Player> {
val updatedPlayer = players[2].copy(score = 100)
mutable[2] = updatedPlayer;
return mutable
}
Où mutable - juste MutableList
, qui est ArrayList
.
$ Java -jar target/benchmarks.jar -f 5 -wi 5 ModifyingImmutableList
Benchmark (size) Mode Cnt Score Error Units
ModifyingImmutableList.baseline 10 thrpt 100 81026110.043 ± 1076989.958 ops/s
ModifyingImmutableList.baseline 100 thrpt 100 81299168.496 ± 910200.124 ops/s
ModifyingImmutableList.baseline 10000 thrpt 100 81854190.779 ± 1010264.620 ops/s
ModifyingImmutableList.baseline 1000000 thrpt 100 83906022.547 ± 615205.008 ops/s
ModifyingImmutableList.toArrayList 10 thrpt 100 33090236.757 ± 518459.863 ops/s
ModifyingImmutableList.toArrayList 100 thrpt 100 11074338.763 ± 138272.711 ops/s
ModifyingImmutableList.toArrayList 10000 thrpt 100 131486.634 ± 1188.045 ops/s
ModifyingImmutableList.toArrayList 1000000 thrpt 100 531.425 ± 18.513 ops/s
Nous avons une régression 2x, et sur 1 million environ 150000x!
Donc, ArrayList
n'est pas le meilleur choix pour les structures de données immuables. Mais il y a beaucoup d'autres collections, l'une d'elles est pcollections . Voyons ce qu'ils ont dans notre scénario:
@Benchmark fun pcollections(): List<Player> {
val updatedPlayer = players[2].copy(score = 100)
return pvector.with(2, updatedPlayer)
}
Où pvector est pvector:PVector<Player> = TreePVector.from(players)
.
$ Java -jar target/benchmarks.jar -f 5 -wi 5 ModifyingImmutableList
Benchmark (size) Mode Cnt Score Error Units
ModifyingImmutableList.baseline 10 thrpt 100 79462416.691 ± 1391446.159 ops/s
ModifyingImmutableList.baseline 100 thrpt 100 79991447.499 ± 1328008.619 ops/s
ModifyingImmutableList.baseline 10000 thrpt 100 80017095.482 ± 1385143.058 ops/s
ModifyingImmutableList.baseline 1000000 thrpt 100 81358696.411 ± 1308714.098 ops/s
ModifyingImmutableList.pcollections 10 thrpt 100 15665979.142 ± 371910.991 ops/s
ModifyingImmutableList.pcollections 100 thrpt 100 9419433.113 ± 161562.675 ops/s
ModifyingImmutableList.pcollections 10000 thrpt 100 4747628.815 ± 81192.752 ops/s
ModifyingImmutableList.pcollections 1000000 thrpt 100 3011819.457 ± 45548.403 ops/s
De bons résultats! Sur 1 million de cas, l'exécution n'est que 27 fois plus lente, ce qui est plutôt cool, mais pour les petites collections pcollections
un peu plus lente que ArrayList
mise en oeuvre.
Update: comme @ mfulton26, dans toMutable
benchmark toList
n'est pas nécessaire, je le supprime et je relance les tests. J'ajoute également un repère sur le coût de création TreePVector
à partir d'un tableau existant
$ Java -jar target/benchmarks.jar ModifyingImmutableList
Benchmark (size) Mode Cnt Score Error Units
ModifyingImmutableList.baseline 10 thrpt 200 77639718.988 ± 1384171.128 ops/s
ModifyingImmutableList.baseline 100 thrpt 200 75978576.147 ± 1528533.332 ops/s
ModifyingImmutableList.baseline 10000 thrpt 200 79041238.378 ± 1137107.301 ops/s
ModifyingImmutableList.baseline 1000000 thrpt 200 84739641.265 ± 557334.317 ops/s
ModifyingImmutableList.iterative 10 thrpt 200 7389762.016 ± 72981.918 ops/s
ModifyingImmutableList.iterative 100 thrpt 200 956362.269 ± 11642.808 ops/s
ModifyingImmutableList.iterative 10000 thrpt 200 10953.451 ± 121.175 ops/s
ModifyingImmutableList.iterative 1000000 thrpt 200 115.379 ± 1.301 ops/s
ModifyingImmutableList.pcollections 10 thrpt 200 15984856.119 ± 162075.427 ops/s
ModifyingImmutableList.pcollections 100 thrpt 200 9322011.769 ± 176301.745 ops/s
ModifyingImmutableList.pcollections 10000 thrpt 200 4854742.140 ± 69066.751 ops/s
ModifyingImmutableList.pcollections 1000000 thrpt 200 3064251.812 ± 35972.244 ops/s
ModifyingImmutableList.pcollectionsFrom 10 thrpt 200 1585762.689 ± 20972.881 ops/s
ModifyingImmutableList.pcollectionsFrom 100 thrpt 200 67107.504 ± 808.308 ops/s
ModifyingImmutableList.pcollectionsFrom 10000 thrpt 200 268.268 ± 2.901 ops/s
ModifyingImmutableList.pcollectionsFrom 1000000 thrpt 200 1.406 ± 0.015 ops/s
ModifyingImmutableList.toArrayList 10 thrpt 200 34567833.775 ± 423910.463 ops/s
ModifyingImmutableList.toArrayList 100 thrpt 200 11395084.257 ± 76689.517 ops/s
ModifyingImmutableList.toArrayList 10000 thrpt 200 134299.055 ± 602.848 ops/s
ModifyingImmutableList.toArrayList 1000000 thrpt 200 549.064 ± 15.317 ops/s
ModifyingImmutableList.toMutable 10 thrpt 200 32441627.735 ± 391890.514 ops/s
ModifyingImmutableList.toMutable 100 thrpt 200 11505955.564 ± 71394.457 ops/s
ModifyingImmutableList.toMutable 10000 thrpt 200 134819.741 ± 526.830 ops/s
ModifyingImmutableList.toMutable 1000000 thrpt 200 561.031 ± 8.117 ops/s
L'interface List
de Kotlin permet "l'accès en lecture seule" à des listes qui ne sont pas nécessairement immuables. L’immuabilité ne peut pas être imposée via des interfaces. L'implémentation actuelle de stdlib de Kotlin pour toList
appelle, dans certains cas, toMutableList
et renvoie son résultat sous la forme d'un "accès en lecture seule" List
.
Si vous avez une List
de joueurs et souhaitez obtenir efficacement une autre List
de joueurs avec un élément mis à jour, une solution simple consiste à copier la liste dans une MutableList
, à mettre à jour l'élément souhaité, puis à ne stocker liste résultante utilisant "l'interface en lecture seule" List
de Kotlin:
val updatedPlayers: List<Player> = players.toMutableList().apply {
this[2] = updatedPlayer
}
Si vous avez souvent l'intention de le faire, vous pouvez envisager de créer une fonction d'extension pour encapsuler les détails de la mise en œuvre:
inline fun <T> List<T>.copy(mutatorBlock: MutableList<T>.() -> Unit): List<T> {
return toMutableList().apply(mutatorBlock)
}
Vous pouvez ensuite copier les listes avec les mises à jour plus facilement (comme pour la copie de classes de données) sans avoir à spécifier explicitement le type de résultat:
val updatedPlayers = players.copy { this[2] = updatedPlayer }
Edit: Avec votre question mise à jour, je dirais que l'utilisation de map
- like est la manière la plus performante de le faire, car elle ne copie la liste qu'une seule fois.
Si vous utilisez mutableListOf
ou des constructeurs normaux tels que ArrayList()
pour créer l'instance, vous pouvez simplement convertir List
en MutableList
:
val mp = players as MutableList<Player>
mp[2] = mp[2].copy(score = 100)
toList
/toMutableList
va dupliquer les éléments de la liste, vous avez donc raison pour l'impact sur les performances.
Cependant, l'idée est que si vous nécessité mutabilité, vous déclarez la propriété en tant que MutableList. Vous pouvez utiliser une construction comme celle-ci - en utilisant deux propriétés - si vous devez exposer la liste à un autre objet:
private val _players = mutableListOf<Player>()
val players: List<Player>
get() = _players.toList()
Pour la variable score
, elle est similaire. Si vous devez la modifier, vous pouvez la déclarer en tant que var
:
data class Player(val name: String, var score: Int = 0)
Dans ce cas, vous pouvez aussi simplement conserver la liste immuable et simplement mettre à jour la valeur:
players[2].score = 100
Vous pouvez trouver plus de détails sur les collections dans les docs: https://kotlinlang.org/docs/reference/collections.html