为什么 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>
}
我是 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>
}