delegatesjvmkotlingeneric-variance

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


I'm new to Kotlin. When I learn Storing Properties in a Map. I try following usage.

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?


Solution

  • Yeah... the compiler is definitely wrong here. (testing with Kotlin version 1.1.2-5)

    First of all, in the case of property delegation to a map, you use the name of the property to look up a value for it in the map.

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

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

    (you mixed the two up)

    A covariant type can be used as a producer. A contravariant type can be used as a consumer. (See PECS. sorry, I don't have a Kotlin specific link, but the principle still applies).

    Delegation by map uses the second generic type of map as a producer (you get things out of the map), so it should not be possible to use a MutableMap<String, in String> since it's second parameter is a consumer (to put things into).

    For some reason, the compiler generates the code it needs for a MutableMap<String, out String> in the case of a MutableMap<String, in String> and this is wrong, 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 cast exception, because the VM is trying to treat a StringBuilder as a String. But you don't use any explicit casts, so it should be safe.

    Unfortunately, it generates garbage (throw null) in the valid use case of out.

    In the case of String it doesn't really make sense to use covariance (out), since String is final, but in the case of a different type hierarchy, the only work around I can think of is to manually patch the bytecode, which is a nightmare.

    I don't know if there is an existing bug report. I guess we'll just have to wait until this gets fixed.