在哪些情况下,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
的代码可能会看到数据发生变化,即使它本身无法进行这些更改。
同理,MutableList
是List
的子接口,MutableSet
是Set
的子接口。
那些顶级接口(例如 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
似乎是 Kotlin 核心数据结构(即 Map、List、Set)
如果我有:
fun <K,V> foo(map: Map<K, V>) {
...
}
我从外面收到后可以map
更改吗?
在哪些情况下可以?
没有
Map
接口不提供任何修改器方法,因此实现 可以 是完全不可变的。
但其他实现不是——特别是,MutableMap
是一个子接口,所以任何实现 that 的东西都是可变的 Map
。这意味着引用 Map
的代码可能会看到数据发生变化,即使它本身无法进行这些更改。
同理,MutableList
是List
的子接口,MutableSet
是Set
的子接口。
那些顶级接口(例如 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