为什么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 ,看看我们可以做这个运算符功能的签名。
同时作为解决方法,您可以将MutableMap
为Map
,以便选择getValue
的第一个重载:
class User(val map: MutableMap<String, out String>) { val name: String by map as Map<String, String> }