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

时间:2017-06-21 01:27:16

标签: delegates jvm kotlin generic-variance

我是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不会给我编译错误?

2 个答案:

答案 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是最终的,但是在不同的类型层次结构中,我只能解决手动修补字节码的问题,这是一场噩梦。

我不知道是否存在错误报告。我想我们只需要等到这个问题得到解决。