我是Kotlin的新手。 当我学习Storing Properties in a Map时。我尝试使用它。
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不会给我编译错误?
答案 0 :(得分:1)
简短回答:这不是编译器中的错误,而是如何为operator getValue()
声明MutableMap
的签名的不幸后果。
答案很长: 由于标准库中有以下三个运算符函数,因此可以delegating properties到映射:
// 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
}
不幸的是,当您尝试使用外推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,看看我们可以通过此运算符函数的签名来完成。
UPD :签名已在Kotlin 1.2.20中修复
同时作为变通方法,您可以将MutableMap
转换为Map
,以便选择getValue
的第一个重载:
class User(val map: MutableMap<String, out String>) {
val name: String by map as Map<String, String>
}
答案 1 :(得分:0)
是的......编译器在这里肯定是错误的。 (使用Kotlin版本1.1.2-5进行测试)
首先,在属性委托给地图的情况下,使用属性的名称在地图中为其查找值。
使用MutableMap<String, in String>
相当于使用逆变的Java Map<String, ? super String>
。
使用MutableMap<String, out String>
相当于使用协方差的Java Map<String, ? extends String>
。
(你把两者混为一谈)
协变类型可以用作生产者。逆变型可以用作消费者。 (见PECS。抱歉,我没有Kotlin特定链接,但原则仍然适用。)
按地图分配使用第二种通用类型的地图作为生产者(你得到地图的 out ),所以不应该使用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
。但是你没有使用任何显式的强制转换,所以它应该是安全的。
不幸的是,它会在throw null
的有效用例中生成垃圾(out
)。
在String
的情况下,使用协方差(out
)并不合理,因为String
是最终的,但是在不同的类型层次结构中,我只能解决手动修补字节码的问题,这是一场噩梦。
我不知道是否存在错误报告。我想我们只需要等到这个问题得到解决。