Почему котлин не разрешает ковариантную mutablemap быть делегатом?

Я новичок в Котлине. Когда я изучу сохранение свойств на карте . Я стараюсь использовать его.

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 } 

Первые два работают, последний не удался. Без модификатора байт-код 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 1.1.2-5)

Прежде всего, в случае делегирования собственности на карту вы используете имя свойства для поиска значения для него на карте.

Использование MutableMap<String, in String> эквивалентно Map<String, ? super String> Java Map<String, ? super String> MutableMap<String, in String> Map<String, ? super String> который использует контравариантность .

Использование MutableMap<String, out String> эквивалентно Map<String, ? extends String> Java Map<String, ? extends String> MutableMap<String, out String> Map<String, ? extends String> который использует ковариацию .

(вы смешали эти два)

Ковариантный тип может использоваться как производитель. Контравариантный тип может использоваться как потребитель. (См. PECS . Извините, у меня нет специальной ссылки Kotlin, но принцип все еще применяется).

Делегирование по карте использует второй общий тип карты как производителя (вы получаете вещи из карты), поэтому не должно быть возможности использовать MutableMap<String, in String> так как вторым параметром является потребитель (чтобы поместить вещи в).

По какой-то причине компилятор генерирует код, необходимый для MutableMap<String, out 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 . Но вы не используете никаких явных приемов, поэтому это должно быть безопасно.

К сожалению, он генерирует мусор ( throw null ) в допустимом случае использования out .

В случае String самом деле нет смысла использовать ковариацию ( out ), так как String является окончательной, но в случае другой иерархии типов единственная работа, о которой я могу думать, – это вручную исправить байт-код, который это кошмар.

Я не знаю, есть ли отчет об ошибке. Думаю, нам просто нужно подождать, пока это не будет исправлено.

Короткий ответ : это не ошибка в компиляторе, а скорее неудачное следствие того, как подпись operator getValue() объявлена ​​для MutableMap .

Длинный ответ : делегирование свойств картам возможно из-за следующих трех функций оператора в стандартной библиотеке:

 // 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 приемника MutableMap выбрана так, что можно делегировать свойство некоторого типа карте, которая может хранить свой супертип:

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

К сожалению, когда вы пытаетесь использовать out-projected 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, чтобы узнать, что мы можем сделать с сигнатурой этой операторской функции.

Между тем в качестве обходного пути вы можете наложить MutableMap на Map , чтобы getValue выбрана первая перегрузка getValue :

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