Kotlin:更新不可变列表元素
Kotlin: Update Immutable List Element
这里是 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 好像没有向量。
编辑:
对于您更新的问题,我想说使用类似 map
的操作是执行此操作的最有效方法,因为它只复制列表一次。
如果您使用 mutableListOf
或 ArrayList()
等普通构造函数来创建实例,您只需将 List
转换为 MutableList
:
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
对我来说显然第二种方式应该更快,但是多少呢?
所以我写了一些基准测试here
@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)
}
}
并获得关注results:
$ 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
所以这个测试表明迭代 collection 大约慢 3~6 倍,即复制。我还提供了我的实现:toArray,它看起来更高效。
在 10 个元素上,toArray
方法每秒具有 33278431.127 ± 560577.516
次操作吞吐量。慢吗?还是速度特别快?我写了 "baseline" 测试,它显示了复制 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 个元素上我们有 2x 回归,在 100 万个元素上有大约 150000x!
所以看起来 ArrayList
不是不可变数据结构的最佳选择。但是还有很多其他的collection,其中之一就是pcollections。让我们看看他们在我们的场景中得到了什么:
@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 倍,这很酷,但是在 collections 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
Kotlin 的 List
interface is for "read-only access" to lists which are not necessarily immutable lists. Immutability cannot be enforced via interfaces. Kotlin's stdlib's current implementation for toList
calls, in some cases, toMutableList
和 returns 其结果为 "read-only access" List
.
如果您有 List
名玩家并希望有效地获得另一 List
名玩家更新元素,那么一个简单的解决方案是将列表复制到 MutableList
,更新所需的元素,然后仅使用 Kotlin 的 "read-only access" 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)
}
然后你可以更流畅地复制带有更新的列表(类似于数据class复制)而不需要明确指定结果类型:
val updatedPlayers = players.copy { this[2] = updatedPlayer }
我不明白为什么要比较这两种方法的相应性能。在第一个中,您将遍历集合的所有元素,在第二个中,您可以通过索引直接找到所需的元素。
遍历不是免费的。
这里是 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 好像没有向量。
编辑:
对于您更新的问题,我想说使用类似 map
的操作是执行此操作的最有效方法,因为它只复制列表一次。
如果您使用 mutableListOf
或 ArrayList()
等普通构造函数来创建实例,您只需将 List
转换为 MutableList
:
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
对我来说显然第二种方式应该更快,但是多少呢?
所以我写了一些基准测试here
@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)
}
}
并获得关注results:
$ 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
所以这个测试表明迭代 collection 大约慢 3~6 倍,即复制。我还提供了我的实现:toArray,它看起来更高效。
在 10 个元素上,toArray
方法每秒具有 33278431.127 ± 560577.516
次操作吞吐量。慢吗?还是速度特别快?我写了 "baseline" 测试,它显示了复制 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 个元素上我们有 2x 回归,在 100 万个元素上有大约 150000x!
所以看起来 ArrayList
不是不可变数据结构的最佳选择。但是还有很多其他的collection,其中之一就是pcollections。让我们看看他们在我们的场景中得到了什么:
@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 倍,这很酷,但是在 collections 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
Kotlin 的 List
interface is for "read-only access" to lists which are not necessarily immutable lists. Immutability cannot be enforced via interfaces. Kotlin's stdlib's current implementation for toList
calls, in some cases, toMutableList
和 returns 其结果为 "read-only access" List
.
如果您有 List
名玩家并希望有效地获得另一 List
名玩家更新元素,那么一个简单的解决方案是将列表复制到 MutableList
,更新所需的元素,然后仅使用 Kotlin 的 "read-only access" 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)
}
然后你可以更流畅地复制带有更新的列表(类似于数据class复制)而不需要明确指定结果类型:
val updatedPlayers = players.copy { this[2] = updatedPlayer }
我不明白为什么要比较这两种方法的相应性能。在第一个中,您将遍历集合的所有元素,在第二个中,您可以通过索引直接找到所需的元素。 遍历不是免费的。