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

7

我是Kotlin的新手。 当我学习在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
}

The first two are both work, the last one failed. With out modifier, the bytecode of getName like this:

  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

As we can see, it will cause a NullPointerException.

Why contravariant is not allowed on a map delegate?

And why kotlin doesn't give me a compile error?


你不应该在那里使用逆变性,因为map会产生值。你没有收到编译器警告的事实很可能是一个错误。实际上,“out”使其协变,“in”使其逆变。出于某种原因,使用“in”会产生我期望使用“out”的代码。 - Jorn Vernee
2个回答

1
简短回答:这不是编译器的错误,而是由于MutableMapoperator getValue()的签名声明方式所导致的不幸后果。 详细回答委托属性到映射中是可能的,因为标准库中有以下三个运算符函数:

// 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,以查看如何处理此运算符函数的签名。 更新:在Kotlin 1.2.20中修复了签名。
同时,您可以将MutableMap强制转换为Map作为解决方法,这样就会选择getValue的第一个重载。
class User(val map: MutableMap<String, out String>) {
    val name: String by map as Map<String, String>
}

0

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

首先,在属性委托到 map 的情况下,您可以使用属性的名称在 map 中查找其值。

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

使用 MutableMap<String, out String> 等同于 Java 的 Map<String, ? extends String>,它使用 协变

(您混淆了两者)

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

通过 map 委托使用 map 的第二个泛型类型作为生产者(从 map 中获取内容),因此不应该使用 MutableMap<String, in 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 的,但对于不同类型层次结构的情况,我唯一能想到的解决方法是手动修补字节码,这是一场噩梦。

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


谢谢。所以这只是一个 bug。 - Dean Xu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接