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

Why kotlin doesn't allow covariant mutablemap to be a delegate?

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

是啊...这里的编译器肯定是错的。 (使用 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 的 link,但原则仍然适用)。

Map 委托使用第二种通用​​类型的 map 作为生产者(你从 map out 得到东西),所以应该不可能使用 MutableMap<String, in String>因为它的第二个参数是一个消费者(把东西放进去)。

出于某种原因,编译器在 MutableMap<String, in String> 的情况下生成 MutableMap<String, out 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
}

您将得到一个 class 转换异常,因为 VM 试图将 StringBuilder 视为 String。但是你没有使用任何显式转换,所以它应该是安全的。

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

String 的情况下,使用协方差 (out) 没有任何意义,因为 String 是最终的,但在不同类型层次结构的情况下, 我能想到的唯一解决方法是手动修补字节码,这是一场噩梦。

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

简短回答:这不是编译器中的错误,而是 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,它是所有可能类型的子类型。
  • 此函数的 return 类型被声明为 V,已被推断为 Nothing,编译器插入一个检查以确保实际的 returned 值是 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>
}