对 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)
,但没有实现。例如,假设 B
是 Int
,而 A
是 Number
。想象一下 mapOfB
实际上是按照一个数组来实现的。 mapOfA.get(3.14159)
当然不能在数组中查找非 Int
键,因为数组是由 Int
s.
索引的
(Kotlin 选择这种设计与 Java 的设计形成对比,我不认为这是正确的举动——但这是他们的选择。Java 的选择是为了get
、containsKey
等采用 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 没有这个问题,因为 get
和 contains
等不接受键类型的参数类型,但接受任何东西。这两种方法各有利弊。它们各自保护您免受不同类型的错误。
这与类型variance有关。考虑这个例子:
val mapOfA: Map<A,A>
val mapOfB = mapOf<B,B>()
mapOfA = mapOfB // assume this is allowed
val item = mapOfA.get(A())
我们在这里做了一些奇怪的事情。两个变量都指向同一张地图,所以我们只要求 mapOfB
获取它的 A
项。但是 mapOfB
对 A
密钥一无所知。它应该与 B
键一起使用。它的 get()
中需要 B
,但我们提供了 A
。因此,我们刚刚打破了类型安全。这就是不允许这样做的原因。
但为什么 toMap()
可以正常工作?因为它创建了地图的副本。现在,向 mapOfA
询问 A
密钥只会询问此副本,而不是 B
的映射。所以这是允许的。
我刚刚有了一个奇怪的发现,想知道为什么它会这样工作。以下代码会引发编译器错误:
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)
,但没有实现。例如,假设 B
是 Int
,而 A
是 Number
。想象一下 mapOfB
实际上是按照一个数组来实现的。 mapOfA.get(3.14159)
当然不能在数组中查找非 Int
键,因为数组是由 Int
s.
(Kotlin 选择这种设计与 Java 的设计形成对比,我不认为这是正确的举动——但这是他们的选择。Java 的选择是为了get
、containsKey
等采用 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 没有这个问题,因为 get
和 contains
等不接受键类型的参数类型,但接受任何东西。这两种方法各有利弊。它们各自保护您免受不同类型的错误。
这与类型variance有关。考虑这个例子:
val mapOfA: Map<A,A>
val mapOfB = mapOf<B,B>()
mapOfA = mapOfB // assume this is allowed
val item = mapOfA.get(A())
我们在这里做了一些奇怪的事情。两个变量都指向同一张地图,所以我们只要求 mapOfB
获取它的 A
项。但是 mapOfB
对 A
密钥一无所知。它应该与 B
键一起使用。它的 get()
中需要 B
,但我们提供了 A
。因此,我们刚刚打破了类型安全。这就是不允许这样做的原因。
但为什么 toMap()
可以正常工作?因为它创建了地图的副本。现在,向 mapOfA
询问 A
密钥只会询问此副本,而不是 B
的映射。所以这是允许的。