【问题标题】:Kotlin. What is the best way to replace element in immutable list?科特林。替换不可变列表中元素的最佳方法是什么?
【发布时间】:2022-12-17 10:19:26
【问题描述】:

更新不可变列表中特定项目的最佳方法是什么。例如,我有Item 的列表。我有几种更新列表的方法:

1.

  fun List<Item>.getList(newItem: Item): List<Item> {
        val items = this.toMutableList()
        val index = items.indexOf(newItem)
        if (index  != -1) {
            items[index ] = newItem
        }
        return items 
    }
fun List<Item>.getList(newItem: Card): List<Item> {
        return this.map { item ->
            if (item.id == newItem.id) newItem else item
        }
    }

第二个选项看起来更简洁,我更喜欢它。但是,在第二个选项中,我们将遍历列表中的每个元素,这对我来说很不利,因为列表可以包含很多元素。

请问,有没有更好的方法来满足我的要求?

【问题讨论】:

    标签: kotlin arraylist collections kotlin-extension


    【解决方案1】:

    第二个看起来性能稍微好一点,但它们都是O(n),所以差别不大,几乎不值得担心。我会选择第二个,因为它更容易阅读。

    第一次迭代列表最多 2 次,但第二次迭代一旦找到项目就会提前中断。 (第一次迭代是复制列表,但 JVM 可能对其进行优化以在后台进行快速数组复制。)

    第二个迭代列表一次,但它必须对列表中的每个项目进行 ID 比较。

    旁注:“不可变”并不是List 的正确术语。它们被称为“只读”列表,因为接口不保证不变性。例如:

    private val mutableList = mutableListOf<Int>()
    
    val readOnlyList: List<Int> get() = mutableList
    

    对于外部类,此 List 是只读的,但不是不可变的。它的内容可能会在拥有该列表的类中进行内部更改。那将是一种脆弱的设计,但这是可能的。在某些情况下,出于性能原因,您可能希望使用 MutableList 并将其传递给只需要只读列表的其他函数。只要您在其他类正在使用它时不对其进行突变,就可以了。

    【讨论】:

    • 谢谢你的答案。也许有比我建议的选项更好的解决方案?
    • 从来没听说过。我猜你可以改变你的第一种方法来手动迭代,但这并没有真正改变行为,而且会更冗长。
    【解决方案2】:

    你有几个选择——你已经在做“制作可变副本并更新它”方法,以及“通过映射每个项目并更改您需要的内容来制作副本”一。

    另一种典型的方法是有点对半,复制你需要的部分,然后插入你想改变的部分。例如,您可以围绕要更改的元素对列表进行切片,然后从这些部分构建最终列表:

    fun List<Item>.update(item: Item): List<Item> {
        val itemIndex = indexOf(item)
        return if (itemIndex == -1) this.toList()
        else slice(0 until itemIndex) + item + slice(itemIndex+1 until size)
    }
    

    通过这种方式,您可以利用底层列表复制方法的任何效率,而 map 必须“转换”每个项目,即使它最终通过原始项目。


    但与往常一样,最好进行基准测试以了解这些方法的实际执行情况! Here's a playground example - 绝对不是进行基准测试的最佳场所,但如果你运行几次,它可以作为一般球场提供指导:

    Mapping all elements: 2500 ms
    Slicing: 1491 ms
    Copy and update index: 611 ms
    

    从广义上讲,映射比切片和组合方法多花费 60-100% 的时间。切片比直接的可变复制和更新花费 2-3 倍的时间。

    考虑到您在这里实际需要做的事情(获取列表的副本并更改(最多)一件事),最后一种方法似乎是最合适的!其他方法各有优势,具体取决于您希望如何操作列表以产生最终结果,但由于您在这里几乎什么都不做,它们只会增加不必要的开销。当然,这取决于您的用例——例如,切片方法比映射方法使用更多的中间列表,除了原始速度之外,这可能是一个问题。


    如果第一个例子中的冗长困扰着你,你总是可以这样写:

    fun List<Item>.getList(newItem: Item): List<Item> =
        this.toMutableList().apply {
            val index = indexOf(newItem)
            if (index != -1) set(index, newItem)
        }
    

    【讨论】:

      【解决方案3】:

      您可以尝试的另一件事是,显然每个项目都有一个 id 字段,您可以使用该字段来标识该项目,从中创建地图,在该地图上执行所有替换,然后将其转换回列表。不过,这仅在您可以批处理所有需要执行的替换时才有用。它也可能会更改列表中项目的顺序。

      fun List<Item>.getList(newItem: Item) =
          associateBy(Item::id)
              .also { map ->
                  map[newItem.id] = newItem
              }
              .values
      

      然后还有可能将您的列表转换为Sequence:这样它将被延迟评估;你用.map添加的每一个替换都会创建一个新的Sequence,它指的是旧的加上你的新映射,并且在你运行一个实际上必须读取整个东西的操作之前,它们都不会被评估,比如@987654326 @.

      【讨论】:

        猜你喜欢
        • 2020-06-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-06-05
        相关资源
        最近更新 更多