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

2

7

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?

Arginine answered 21/6, 2017 at 1:27 Comment(1)
You shouldn't be able to use contravariance there, since the map produces values. The fact that you don't get a compiler warning is likely a bug. Actually out makes in covariant, in makes it contravariant. For some reason, using in produces the code I would expect with out.Nagaland
N
0

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.

Nagaland answered 21/6, 2017 at 12:47 Comment(1)
Thanks. So it just a bug.Arginine
C
1

Short answer: it's not a bug in the compiler, but rather an unfortunate consequence of how the signature of operator getValue() is declared for MutableMap.

Long answer: delegating properties to maps is possible because of 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 use-site variance of the MutableMap receiver is chosen so that one could delegate a property of some type to a map which 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 an out-projected MutableMap<String, out String> as a delegate for a val property and consequently as a receiver of getValue operator, here what happens:

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

I've opened an issue KT-18789 to see what we can be done with the signature of this operator function.

UPD: The signature was fixed in Kotlin 1.2.20

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

class User(val map: MutableMap<String, out String>) {
    val name: String by map as Map<String, String>
}
Cumming answered 12/7, 2017 at 17:0 Comment(0)
N
0

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.

Nagaland answered 21/6, 2017 at 12:47 Comment(1)
Thanks. So it just a bug.Arginine

© 2022 - 2024 — McMap. All rights reserved.