【问题标题】:Confusion about Maps and generics关于地图和泛型的困惑
【发布时间】:2021-10-19 16:55:23
【问题描述】:

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

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。这是怎么回事?

【问题讨论】:

  • B 是否扩展 A
  • 哦,该死的,忘了把那个细节放进去。刚刚更新了问题来表明这一点。

标签: list dictionary kotlin generics data-structures


【解决方案1】:

考虑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&lt;K, out V&gt;的定义中特别指定的:允许向上转换V,但不允许向上转换K

【讨论】:

  • 如果我错了,请纠正我,但是尽管 Java 允许 Object 键作为方法参数,但键类型仍然是不变的(并且值类型也是不变的,因为没有声明站点差异),所以你仍然无法将Map&lt;B, B&gt; 分配给Map&lt;A, A&gt; 变量,只能分配Map&lt;? extends A, ? extends A&gt; 变量。但不幸的是,在 Kotlin Map&lt;out A, A&gt; 中执行等效操作会使您的地图无法用于检索值。
  • 这是关于可变性的,真的。 Java 中的等价物将包装在 Collections.unmodifiableMap 中,它 键的变体。
【解决方案2】:

这与类型 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 的映射。所以这是允许的。

【讨论】:

  • 有趣。那么使这项工作发挥作用的编译时机制是什么?据我了解,静态类型分析不考虑两个变量是否实际引用同一个对象。它只考虑每个对象的声明类型,对吗?
  • 我仍然很困惑,因为如果你调用mapOfB.toMap(),它仍然是一张带有B 对象的地图,mapOfA 被设置为。那么这仍然不会造成问题吗?
  • 不,这不是由跟踪引用处理的。这是由类型系统处理的。当您将mapOfB 分配给mapOfA 时,您需要投射地图。并且根据差异,它可能被允许或不允许。例如,List&lt;B&gt; 可以安全地转换为List&lt;A&gt;,因为我们从中得到的任何B 项目都与A 相同。但是我们不能将MutableList&lt;B&gt; 转换为MutableList&lt;A&gt;,因为它允许将A 项放入B 项的列表中。在地图中,参数K 是不变的——这意味着它不能安全地转换为任何其他类型。
  • 是的,地图副本仍将仅包含 B 键。但这与变量的真实内容并不真正相关,而是与您明确允许它存储的内容有关。当您创建Map&lt;B, B&gt; 时,这就像在说:“我禁止任何人将我的地图与B 之外的任何其他键一起使用”。当您创建地图的副本时,此新地图不受此约束,您可以更改其类型。另外,假设您实现了自己的 Map 类,该类只能与 B 键一起使用。复制后,副本(可能是HashMap)不再受你的实现限制,可以存储A的。
【解决方案3】:

Map 的键的类型是不变的。这意味着Map&lt;B, B&gt; 不是Map&lt;A, B&gt;Map&lt;A, A&gt;,因为您不能向上转换不变量类型。从理论上讲,正在使用的 Map 接口的实现在传递错误类型的键时可能会崩溃,就像你传递给它的不是 B 的 A 子类型一样。

当您调用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等不接受key类型的参数类型,而是接受任何东西。这两种方法各有利弊。它们都可以保护您免受不同类型的错误的侵害。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-04-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-10-30
    • 2011-12-13
    • 2017-12-04
    相关资源
    最近更新 更多