Дженерики
Стирание типов, reified-параметры типа и вариантность в дженериках Kotlin.
7 вопросов
JuniorТеорияОчень частоЧто означают out и in у параметра типа в Kotlin?
Что означают out и in у параметра типа в Kotlin?
Они объявляют вариантность один раз, на самом классе. out T разрешает T только в позициях-производителях — вернуть T можно, принять нельзя — и делает Box<Dog> пригодным как Box<Animal>. in T разрешает T только в позициях-потребителях и переворачивает это. Без них тип инвариантен.
Типичные ошибки
- ✗Путать их местами — ждать, что класс с
out Tвсё ещё приметTкак параметр функции - ✗Считать, что дженерик-класс ковариантен по умолчанию, как массив в Java
- ✗Думать, что вариантность проверяется в рантайме, а не компилятором в объявлении
Уточняющие вопросы
- →Почему
MutableList<T>инвариантен, аList<T>объявлен сout T? - →Что написать в одном месте использования, если сам класс нельзя объявить вариантным?
JuniorТеорияЧастоЧто такое стирание типов, и почему нельзя написать T::class.java для обычного дженерика T?
Что такое стирание типов, и почему нельзя написать T::class.java для обычного дженерика T?
На JVM аргумент типа дженерика стирается при компиляции — во время выполнения List<String> — это просто List. Поэтому у обычного параметра типа T нет runtime-токена класса; T::class.java не компилируется, ведь реальный тип в рантайме неизвестен.
Типичные ошибки
- ✗Считать, что аргументы типа доживают до рантайма, как в шаблонах C++
- ✗Думать, что
T::classработает внутри любой generic-функции - ✗Путать стирание с выводом типов, который тоже происходит при компиляции
Уточняющие вопросы
- →Какая возможность Kotlin позволяет функции восстановить аргумент типа в рантайме?
- →Почему две перегрузки, различающиеся лишь аргументом типа, конфликтуют после стирания?
MiddleДебаггингЧастоКак починить filterWrapper, чтобы он фильтровал по T без рефлексии?
Как починить filterWrapper, чтобы он фильтровал по T без рефлексии?
Обычный T стирается, поэтому T::class.java недоступен. Сделайте функцию inline, а параметр — reified: inline fun <reified T> filterWrapper(data: List<Any>) = data.filterIsInstance<T>(). Встраивание подставляет реальный тип T в место вызова — без рефлексии.
Типичные ошибки
- ✗Пробовать
T::class.javaв не-inline функции, гдеTстирается - ✗Думать, что спасёт только передача
Class<T>, упускаяreified - ✗Помечать параметр
reifiedбезinlineу функции
Уточняющие вопросы
- →Почему
reified-параметр обязан быть уinline-функции? - →Какую runtime-проверку выполняет
filterIsInstance<T>()для каждого элемента?
JuniorТеорияИногдаЧто вводит typealias в Kotlin, а что — нет?
Что вводит typealias в Kotlin, а что — нет?
Он вводит альтернативное имя для существующего типа — и больше ничего. Компилятор разворачивает псевдоним обратно в исходный тип, поэтому не появляется ни нового типа, ни нового класса, ни объекта-обёртки. typealias UserId = String и String взаимозаменяемы везде.
Типичные ошибки
- ✗Ждать, что компилятор отвергнет обычный
Stringтам, где объявленtypealias UserId = String - ✗Думать, что
typealiasсоздаёт класс или обёртку и потому что-то стоит в рантайме - ✗Путать его с
value class, который как раз вводит отдельный тип, проверяемый компилятором
Уточняющие вопросы
- →Для чего
typealiasреально хорош, если он не добавляет никакой типобезопасности? - →Можно ли объявить
typealiasвнутри класса или тела функции, и почему именно так?
MiddleТеорияИногдаЧем out/in на объявлении в Kotlin отличаются от ? extends/? super в Java?
Чем out/in на объявлении в Kotlin отличаются от ? extends/? super в Java?
Kotlin задаёт вариантность один раз, на классе, и любое место использования получает её даром. В Java такого нет: каждое место обязано повторять ? extends T или ? super T. Обе живут только при компиляции — стирание оставляет один сырой тип, — поэтому Kotlin выпускает wildcards на границе с Java.
Типичные ошибки
- ✗Думать, что в Kotlin вовсе нет проекций в месте использования, а есть только форма на объявлении
- ✗Считать, что вариантность переживает стирание и проверяется JVM в рантайме
- ✗Ждать, что класс Kotlin с
out Tпопадёт в Java-сигнатуры без wildcards
Уточняющие вопросы
- →Когда компилятор не выпускает wildcard и что меняет
@JvmSuppressWildcards? - →Почему правило «производитель — extends, потребитель — super»
PECSне нужно для класса сout T?
MiddleТеорияИногдаКогда typealias не даёт той типобезопасности, которую дал бы value class?
Когда typealias не даёт той типобезопасности, которую дал бы value class?
Всегда — это имя, а не тип. typealias UserId = String принимает любой String, а UserId свободно смешивается с OrderId, ведь оба разворачиваются в String; совместимость типов и вариантность остаются теми же, что у исходного типа. А value class UserId(val v: String) — отдельный тип, который проверяет компилятор.
Типичные ошибки
- ✗Ждать, что компилятор отвергнет
OrderIdтам, где объявленUserId - ✗Думать, что
typealiasможет сузить или поменять вариантность типа, который он именует - ✗Считать, что
value classвсегда выделяет обёртку, и брать псевдоним ради экономии
Уточняющие вопросы
- →Когда
value classвынужден боксироваться, теряя представление без обёртки? - →Ради чего
typealiasвсё-таки стоит применять, если своей безопасности он не добавляет?
MiddleТеорияРедкоЧто позволяет star-проекция List<*> и что она запрещает?
Что позволяет star-проекция List<*> и что она запрещает?
List<*> говорит, что тип элемента — некий единый, но неизвестный тип. Чтение разрешено: каждый элемент возвращается как Any?, верхняя граница. Запись отвергается, ведь компилятор не может доказать, что ваше значение подходит под этот неизвестный тип, — MutableList<*>.add(x) не компилируется.
Типичные ошибки
- ✗Читать
List<*>какList<Any?>и ждать, что в него можно добавить элемент - ✗Думать, что
*— это сырой тип Kotlin, разрешающий непроверяемую запись под предупреждение - ✗Ждать, что чтение вернёт конкретный тип элемента, а не верхнюю границу
Уточняющие вопросы
- →Чем
MutableList<*>отличается отMutableList<Any?>в месте вызова? - →Во что проецируется
*для параметра, объявленного какin T, а неout T?