7

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

4

2 回答 2

1

简短的回答:这不是编译器中的错误,而是如何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
}

不幸的是,当您尝试使用外投影MutableMap<String, out String>作为val属性的委托并因此作为getValue运算符的接收者时,会发生以下情况:

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

我已经打开了一个问题KT-18789 来看看我们可以用这个操作符函数的签名来做些什么。

UPD:签名已在 Kotlin 1.2.20 中修复

同时,作为一种解决方法,您可以强制MutableMap转换 to Map,以便getValue选择第一个重载:

class User(val map: MutableMap<String, out String>) {
    val name: String by map as Map<String, String>
}
于 2017-07-12T17:00:06.437 回答
0

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

首先,在将属性委托给映射的情况下,您使用属性的名称在映射中为其查找值。

使用MutableMap<String, in String>, 等效于Map<String, ? super String>使用逆变的 Java 。

使用MutableMap<String, out String>, 等效于Map<String, ? extends String>使用协方差的 Java 。

(你把两者混在一起了)

协变类型可以用作生产者。逆变类型可以用作消费者。(请参阅PECS。抱歉,我没有 Kotlin 特定链接,但原则仍然适用)。

映射委托使用第二种通用​​类型的映射作为生产者(您可以映射中获取东西),因此应该不可能使用 a ,MutableMap<String, in String>因为它的第二个参数是消费者(将东西放入)。

出于某种原因,编译器会在 aMutableMap<String, out String>的情况下生成 a 所需的代码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
}

您将得到一个类转换异常,因为 VM 试图将 aStringBuilder视为String. 但是您不使用任何显式强制转换,因此它应该是安全的。

不幸的是,它throw nullout.

在使用协String方差(.outString

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

于 2017-06-21T12:47:04.693 回答