Kotlin:更新不可变列表元素

Kotlin初学者在这里。 我如何获取一个列表,而不用改变它,创建一个更新元素在特定索引的第二个(不可变)列表?

我想到了两种方式,这两种方式似乎都可能导致性能命中,改变底层对象,或两者兼而有之。

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() 

如果没有高性能的方法来做到这一点,Kotlin stdlib或其他库中是否有更合适的数据结构? Kotlin似乎没有载体。

Kotlin的List接口用于“只读访问”列表,这些列表不一定是不可变的列表。 不可变性不能通过接口强制执行。 Kotlin的stdlib 当前的实现 toList调用,在某些情况下, toMutableList并返回其结果作为“只读访问” List

如果你有一个玩家List并希望有效地得到更新元素的另一个玩家List ,那么一个简单的解决方案是将列表复制到MutableList ,更新所需的元素,然后只使用Kotlin “只读访问” List界面:

 val updatedPlayers: List<Player> = players.toMutableList().apply { this[2] = updatedPlayer } 

如果这是你打算经常做的事情,你可以考虑创建一个扩展函数来封装实现细节:

 inline fun <T> List<T>.copy(mutatorBlock: MutableList<T>.() -> Unit): List<T> { return toMutableList().apply(mutatorBlock) } 

然后,您可以更新流利地复制列表(类似于数据类复制),而无需明确指定结果类型:

 val updatedPlayers = players.copy { this[2] = updatedPlayer } 

对我来说显而易见,第二种方式应该更快,但多少?

所以我在这里写了一些基准

 @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) } } 

得到如下结果 :

 $ 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 

所以这个测试表明,迭代收集慢3〜6倍,即复制。 另外我提供了我的实现: toArray ,看起来更高性能。

在10元素上, toArray方法的吞吐量为33278431.127 ± 560577.516次。 它慢吗? 或者它非常快? 我写“基准”测试,显示复制Players和变异数组的成本。 结果有趣:

 @Benchmark fun baseline(): List<Player> { val updatedPlayer = players[2].copy(score = 100) mutable[2] = updatedPlayer; return mutable } 

凡可变 – 只是MutableList ,这是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 

在10个元素上,我们有2个回归,在100万个大约150000x!

所以看起来像ArrayList不是不可变数据结构的最佳选择。 但是还有很多其他的收藏品,其中之一就是收藏品 。 让我们看看他们在我们的场景中得到了什么:

 @Benchmark fun pcollections(): List<Player> { val updatedPlayer = players[2].copy(score = 100) return pvector.with(2, updatedPlayer) } 

其中pvector是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 

好的结果! 在100万的情况下,我们只有27倍的执行速度,这非常酷,但是对于小集合pcollections ,它比ArrayList实现慢了一点。

更新 :作为@ mfulton26提到,在toMutable基准toList是不必要的,所以我删除它,并重新运行测试。 另外我添加基准创建TreePVector从现有的数组的成本:

 $ 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 

编辑:与您更新的问题,我会说,使用类似map的操作是执行此操作的最高性能的方式,因为它只复制一次列表。


如果使用mutableListOf或像ArrayList()这样的普通构造函数来创建实例,则可以简单地将ListMutableList

 val mp = players as MutableList<Player> mp[2] = mp[2].copy(score = 100) 

toList / toMutableList将复制列表项目,所以你是正确的性能影响。

然而,这个想法实际上是,如果你需要可变性,你声明属性为MutableList。 你可以使用这样的结构 – 使用两个属性 – 如果你需要公开列表到另一个对象:

 private val _players = mutableListOf<Player>() val players: List<Player> get() = _players.toList() 

对于score变量,它是相似的 – 如果你需要改变它,可以声明为var

 data class Player(val name: String, var score: Int = 0) 

在这种情况下,您也可以只保留不变的列表并更新值:

 players[2].score = 100 

您可以在文档中找到有关收藏集的更多详细信息: https : //kotlinlang.org/docs/reference/collections.html

Interesting Posts