Why does Kotlin not allow the covariant mutablemap to be a delegate?

I am new to Kotlin. When I find out Saving properties on a map . I try to use it.

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 of them work, the last of them failed. With the out modifier, the getName as follows:

  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, this will result in a NullPointerException .

Why is contra-option not allowed on card deletion?

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

+7
jvm delegates kotlin generic-variance
source share
2 answers

Yes ... the compiler is definitely wrong. (testing with Kotlin version 1.1.2-5)

First of all, in the case of delegating ownership of a map, you use the name of the property to find the value for it on the map.

Using MutableMap<String, in String> equivalent to Java Map<String, ? super String> Map<String, ? super String> , which uses contravariance.

Using MutableMap<String, out String> equivalent to Java Map<String, ? extends String> Map<String, ? extends String> , which uses covariance.

(you mixed twice)

As a manufacturer, a covariant type can be used. The contravariant type can be used as a consumer. (See PECS . Sorry, I don't have a special Kotlin link, but the principle still applies.)

Delegating by map uses the second general type of map as the producer (you get things from the map), so you should not use MutableMap<String, in String> , since the second parameter is the consumer (to insert things into).

For some reason, the compiler generates the code that it needs for MutableMap<String, out String> in the case of MutableMap<String, in String> , and this is incorrect, as you can see in this example:

 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 } 

You will get a class exception because the VM is trying to treat StringBuilder as String . But you are not using any explicit tricks, so this should be safe.

Unfortunately, it generates throw null ( throw null ) in a valid example of using out .

In the case of String it makes no sense to use covariance ( out ), since String is final, but in the case of a different type hierarchy, the only work I can think of is manually fixing the bytecode, which is a nightmare.

I do not know if there is an existing error report. I think we just need to wait until it gets better.

0
source share

Short answer : this is not a compiler error, but rather an unfortunate consequence of how operator getValue() MutableMap for MutableMap .

Long answer : delegating properties to maps is possible due to the following three operator functions in the standard library:

 // 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) 

Here, the variance of the used site of the MutableMap receiver MutableMap selected so that you can delegate a property of some type to a map that can store its supertype:

 class Sample(val map: MutableMap<String, Any>) { var stringValue: String by map var intValue: Int by map } 

Unfortunately, when you try to use out-projected MutableMap<String, out String> as a delegate for the val property, and therefore, as the recipient of the getValue operator, this is what happens:

  • MutableMap<in String, in V>.getValue overload is selected because it has a more specific receiver type.
  • Since the receiver map has a projection of the out String argument, it is not known what its actual type argument is (it can be MutableMap<..., String> or MutableMap<..., SubTypeOfString> ), so the only safe option is its Nothing , which is subtype of all possible types.
  • The return type of this function is declared as V , which has been output to Nothing , and the compiler inserts a check that the actual return value is of type Nothing , which should always fail, because it cannot be a value of type Nothing . This check looks like throw null in bytecode.

I opened problem KT-18789 to see what we can do with the signature of this operator function.

Meanwhile, as a workaround, you can drop the MutableMap to Map so that the first getValue overload is getValue :

 class User(val map: MutableMap<String, out String>) { val name: String by map as Map<String, String> } 
+1
source share

All Articles