为什么kotlin不允许covariant mutablemap成为委托?

我是Kotlin的新手。 当我学习在地图中存储属性 。 我尝试下面的使用。

class User(val map: MutableMap<String, String>) { val name: String by map } 

 class User(val map: MutableMap<String, in String>) { val name: String by map } 

 class User(val map: MutableMap<String, out String>) { val name: String by map } 

前两个都是工作,最后一个失败了。 用out修饰符, getName的字节码就像这样:

  public final java.lang.String getName(); 0 aload_0 [this] 1 getfield kotl.User.name$delegate : java.util.Map [11] 4 astore_1 5 aload_0 [this] 6 astore_2 7 getstatic kotl.User.$$delegatedProperties : kotlin.reflect.KProperty[] [15] 10 iconst_0 11 aaload 12 astore_3 13 aload_1 14 aload_3 15 invokeinterface kotlin.reflect.KProperty.getName() : java.lang.String [19] [nargs: 1] 20 invokestatic kotlin.collections.MapsKt.getOrImplicitDefaultNullable(java.util.Map, java.lang.Object) : java.lang.Object [25] 23 checkcast java.lang.Object [4] 26 aconst_null 27 athrow Local variable table: [pc: 0, pc: 28] local: this index: 0 type: kotl.User 

正如我们所看到的,这将导致一个NullPointerException

为什么地图代表不允许使用逆变?

为什么kotlin不会给我一个编译错误?

是的,编译器在这里肯定是错误的。 (使用Kotlin 1.1.2-5版进行测试)

首先,在属性委托给地图的情况下,您可以使用属性的名称在地图中为其查找值。

使用MutableMap<String, in String> ,相当于Java的Map<String, ? super String> MutableMap<String, in String> Map<String, ? super String>使用反变换

使用MutableMap<String, out String> ,相当于Java的Map<String, ? extends String> MutableMap<String, out String> Map<String, ? extends String>使用协方差的 Map<String, ? extends String>

(你把两个混起来)

协变类型可以用作生产者。 逆变类型可以用作消费者。 (见PECS 。对不起,我没有Kotlin特定的链接,但是这个原则仍然适用)。

按照映射的委派使用第二个通用类型的映射作为生产者(你从映射中得到东西),所以不应该使用一个MutableMap<String, in String>因为它的第二个参数是一个消费者成)。

出于某种原因,编译器为MutableMap<String, out String>生成MutableMap<String, in String> ,这是错误的,如下例所示:

 class User(val map: MutableMap<String, in String>) { val name: String by map } fun main(args:Array<String>){ val m: MutableMap<String, CharSequence> = mutableMapOf("name" to StringBuilder()) val a = User(m) val s: String = a.name } 

你会得到一个类抛出异常,因为虚拟机试图把一个StringBuilder作为一个String 。 但是你不使用任何明确的转换,所以它应该是安全的。

不幸的是,它在out的有效用例中生成垃圾( throw null )。

String的情况下,使用协变( out )是没有意义的,因为String是final的,但是对于不同的类型层次结构,我唯一能想到的工作就是手工修补字节码,是一场噩梦。

我不知道是否有一个现有的错误报告。 我想我们只能等到这个问题得到解决。

简单的回答 :这不是编译器中的错误,而是operator getValue()的签名如何为MutableMap声明的一个不幸的后果。

长答案 :由于标准库中有以下三个运算符函数,可以将属性委托给映射:

 // for delegating val to read-only map operator fun <V, V1: V> Map<in String, @Exact V>.getValue(thisRef: Any?, property: KProperty<*>): V1 // for delegating var to mutable map operator fun <V> MutableMap<in String, in V>.getValue(thisRef: Any?, property: KProperty<*>): V operator fun <V> MutableMap<in String, in V>.setValue(thisRef: Any?, property: KProperty<*>, value: V) 

这里选择MutableMap接收器的使用地点差异,以便可以将某种类型的属性委托给可以存储其超类型的地图:

 class Sample(val map: MutableMap<String, Any>) { var stringValue: String by map var intValue: Int by map } 

不幸的是,当你试图使用out-projected MutableMap<String, out String>作为一个val属性的委托,因此作为getValue运算符的接收器,在这里会发生什么:

  • MutableMap<in String, in V>.getValue重载被选中,因为它具有更多特定的接收者类型。
  • 由于接收者映射有out String类型的参数投影,所以它的实际类型参数是什么(它可以是MutableMap<..., String>MutableMap<..., SubTypeOfString> ),所以唯一安全的选择是假设它是Nothing ,这是所有可能类型的子类型。
  • 这个函数的返回类型被声明为V ,它已经被推断为Nothing ,并且编译器插入一个检查实际返回的值是Nothing类型,它总是失败,因为不可能有一个Nothing类型的值。 这个检查看起来像字节码中的throw null

我已经打开了一个问题KT-18789 ,看看我们可以做这个运算符功能的签名。

同时作为解决方法,您可以将MutableMapMap ,以便选择getValue的第一个重载:

 class User(val map: MutableMap<String, out String>) { val name: String by map as Map<String, String> }