对 Maps 和泛型的困惑

Confusion about Maps and generics

我刚刚有了一个奇怪的发现,想知道为什么它会这样工作。以下代码会引发编译器错误:

interface A
class B: A
val mapOfA: Map<A,A>
val mapOfB = mapOf<B,B>()
mapOfA = mapOfB

你得到

Type mismatch.
   Required: Map<A, A>
   Found: Map<B, B>

但是这段代码有效。

val mapOfA: Map<A,A>
val mapOfB = mapOf<B,B>()
mapOfA = mapOfB.toMap()

唯一不同的是我现在正在打电话 mapOfB.toMap()mapOfB 已经是 Map 那么为什么这会改变什么?我正在使用 Kotlin 版本 1.5.10。这是怎么回事?

考虑 mapOfB.get。这个accepts a B and only a B.

很有可能 mapOfB 的实现 不能 支持 get(A),但没有实现。例如,假设 BInt,而 ANumber。想象一下 mapOfB 实际上是按照一个数组来实现的。 mapOfA.get(3.14159) 当然不能在数组中查找非 Int 键,因为数组是由 Ints.

索引的

(Kotlin 选择这种设计与 Java 的设计形成对比,我不认为这是正确的举动——但这是他们的选择。Java 的选择是为了getcontainsKey 等采用 Object 论点,这会导致 this 等问题。)

Map<K, out V>的定义中明确规定:允许向上转型V,但不允许K

地图键的类型是不变的。这意味着 Map<B, B> 不是 Map<A, B>Map<A, A> 因为你不能向上转换不变类型。理论上,正在使用的 Map 接口的实现在向它传递错误类型的键时可能会崩溃,就像你向它传递了 A 的某个子类型而不是 B 一样。

当您调用 toMap 时,它会创建一个新的 Map,已知使用超类型 A 作为 Key 是安全的,因此它可以安全地向上转换类型。在引擎盖下,它将每个条目转移到一个新地图,所以它基本上是向上转换每个键以键入 A.


以下是类型安全保护您的示例:

interface A
class B(val name: String): A
class C: A

class MyMap: HashMap<B, B>() {
    override fun get(key: B): B? {
        println("I'm returning ${key.name}")
        return super.get(key)
    }
}

如果您现在这样做并且编译器让您:

val a = Map<A, A>
val b: Map<B, B> = MyMap()
a = b // imagine this is allowed.
val x = a[C()] // Crash. C cannot be cast to B inside the MyMap.get() function

如果您使用 toMap(),一个新的 Map 是从头开始创建的,它不会有这个问题,所以编译器可以安全地转换键类型。

Java 没有这个问题,因为 getcontains 等不接受键类型的参数类型,但接受任何东西。这两种方法各有利弊。它们各自保护您免受不同类型的错误。

这与类型variance有关。考虑这个例子:

val mapOfA: Map<A,A>
val mapOfB = mapOf<B,B>()
mapOfA = mapOfB // assume this is allowed

val item = mapOfA.get(A())

我们在这里做了一些奇怪的事情。两个变量都指向同一张地图,所以我们只要求 mapOfB 获取它的 A 项。但是 mapOfBA 密钥一无所知。它应该与 B 键一起使用。它的 get() 中需要 B,但我们提供了 A。因此,我们刚刚打破了类型安全。这就是不允许这样做的原因。

但为什么 toMap() 可以正常工作?因为它创建了地图的副本。现在,向 mapOfA 询问 A 密钥只会询问此副本,而不是 B 的映射。所以这是允许的。