在哪些情况下,Kotlin 核心数据结构(Map、List、Set)不是真正不可变的?

In which cases Kotlin core Data Structures (Map, List, Set) are not really immutable?

似乎是 Kotlin 核心数据结构(即 Map、List、Set)

如果我有:

  fun <K,V> foo(map: Map<K, V>) {
    ...
  }

我从外面收到后可以map更改吗?

在哪些情况下可以?

没有

Map 接口不提供任何修改器方法,因此实现 可以 是完全不可变的。

但其他实现不是——特别是,MutableMap 是一个子接口,所以任何实现 that 的东西都是可变的 Map。这意味着引用 Map 的代码可能会看到数据发生变化,即使它本身无法进行这些更改。

同理,MutableListList的子接口,MutableSetSet的子接口。

那些顶级接口(例如 kotlinx.collections.immutable and Guava 库)有不可变的实现——您可以编写自己的。但是 Kotlin 语言和类型系统还没有为深度不变性提供强大的支持,仅针对可能不变或可能不可变的数据的只读接口提供支持。

(这并不是说以后不能添加这样的支持。有很多interest in it, and JetBrains have been considering。)

让我们运行做个实验:

class Foo {

    @Test
    fun foo() {
        val items = mutableListOf("A")
        run(items)
        Thread.sleep(1000)
        items.add("B")
        println("Foo")
        Thread.sleep(2000)
    }

    fun run(items: List<String>) {
        thread(start = true) {
            println("Run ${items.count()}")
            Thread.sleep(2000)
            println("Run ${items.count()}")
        }
    }
}

此测试用例将创建一个包含 1 个项目的可变列表,然后它将对该列表的引用传递到一个类型为不可变列表的方法中。

此方法调用 运行 将显示列表的长度。

在 运行 方法之外,一个新项目将附加到列表中。

已添加睡眠确保添加到列表中发生在 运行 的第一个语句之后但在第二个 print 语句之前。

让我们检查一下输出:

Run 1
Foo
Run 2

正如我们所见,列表内容确实发生了变化,即使 运行 接受了一个不可变列表。

这是因为 MutableList 和 List 只是接口,所有 MutableList 实现也实现了 List。

当 Kotlin 提到可变和不可变时,它只是指是否存在修改集合的方法,而不是内容是否可以更改。

因此,如果您使用 List 作为参数类型将列表引入一个方法,那么是的,如果它们被另一个线程更改,内容可能会有所不同,如果这是一个问题,那么复制一个列表作为你的方法做的第一件事。

正如其他人指出的那样,当您在另一个线程中使用地图时可以对其进行修改...但是除非您对地图的访问权限为 @Synchronized,否则这已经被破坏了,这表明你知道它会改变,所以这种可能性并不是真正的问题。即使您的方法采用了 MutableMap 参数,如果在您的方法执行过程中更改了参数,那也是错误的。

我认为您误解了只读集合接口的用途。

当您的方法 接受 一个 Map 作为参数时,您表示该方法不会更改地图。只读Map接口的目的就是让你说出这样的话。你 可以 (map as? MutableMap)?.put(...),但那是错误的,因为你承诺不会那样做。您还可以通过各种方式使进程崩溃或 运行 无限循环,但这也是错误的。只是不要这样做。该语言不提供针对 恶意 程序员的保护。

同理,如果你的方法returns a Map,那就说明接收者不能改变它。 通常 在这种情况下,您还承诺(希望在评论中)returned 地图不会更改。如果收到地图的任何人都可以自己更改它,那么您就无法遵守此承诺,这就是为什么您 return Map 而不是基础 MutableMap