--- title: Reified и inline description: Как reified обходит стирание типов, что компилятор делает с inline-функциями и чего reified не умеет tags: - kotlin - generics --- На JVM параметры типа стираются: в рантайме `List` и `List` - один класс. Но если функция объявлена `inline`, компилятор подставляет её тело в место вызова, где фактический тип **известен**. Это и позволяет `reified`. ```kotlin inline fun isOfType(value: Any): Boolean = value is T fun main() { println(isOfType("текст")) // true println(isOfType("текст")) // false println(isOfType>(listOf(1))) // true } ``` Без `reified` строка `value is T` не компилируется - проверить стёртый тип невозможно. ## Что делает компилятор ```kotlin inline fun nameOf(): String = T::class.simpleName ?: "?" fun main() { println(nameOf()) // String println(nameOf()) // Int } ``` В байт-коде вызова `nameOf()` не остаётся: компилятор подставляет тело и заменяет `T` на `String`, получая буквально `String::class.simpleName`. Поэтому `reified` и требует `inline` - без подстановки подставлять нечего. Следствие: `reified`-функцию нельзя вызвать из Java и нельзя передать `T` динамически. ## Главный случай: парсинг и десериализация Задача, ради которой `reified` чаще всего и используют: ```kotlin inline fun parseAs(raw: String): T? = when (T::class) { Int::class -> raw.toIntOrNull() as T? Long::class -> raw.toLongOrNull() as T? Double::class -> raw.toDoubleOrNull() as T? Boolean::class -> raw.toBooleanStrictOrNull() as T? String::class -> raw as T else -> null } fun main() { println(parseAs("42")) // 42 println(parseAs("abc")) // null println(parseAs("1.5")) // 1.5 println(parseAs("true")) // true println(parseAs("текст")) // текст } ``` В Java такая функция потребовала бы параметр `Class type` в сигнатуре. Именно это делают Jackson (`readValue(json, User.class)`) и Gson (`fromJson(json, User.class)`); Kotlin-обёртки над ними объявляют `inline fun readValue(json: String): T`, и тип выводится из контекста: ```kotlin // С kotlinx.serialization или Jackson Kotlin module: // val user: User = mapper.readValue(json) // тип берётся из объявления переменной ``` ## filterIsInstance и прочее в stdlib ```kotlin fun main() { val mixed: List = listOf(1, "a", 2.0, 3, "b") println(mixed.filterIsInstance()) // [1, 3] println(mixed.filterIsInstance()) // [a, b] println(mixed.filterIsInstance()) // [1, 2.0, 3] val target = mutableListOf() mixed.filterIsInstanceTo(target) println(target) // [1, 3] } ``` `filterIsInstance` объявлена как `inline fun Iterable<*>.filterIsInstance(): List` - без `reified` поэлементная проверка типа была бы невозможна. Другие `reified`-функции stdlib: ```kotlin fun main() { val array = arrayOf(1, 2, 3) // arrayOf — reified println(listOf("a", "b").toTypedArray().size) // 2 — тоже reified println(array.javaClass.simpleName) // Integer[] val typed: Array = arrayOfNulls(2) println(typed.size) // 2 println(emptyArray().size) // 0 } ``` `toTypedArray` без `reified` не смогла бы создать массив правильного типа - у `Array` в рантайме есть настоящий тип элементов. ## Безопасное приведение ```kotlin inline fun Any?.asOrNull(): T? = this as? T fun main() { val value: Any = "текст" println(value.asOrNull()?.length) // 5 println(value.asOrNull()) // null — без исключения } ``` Это удобнее, чем `as? String` в каждом месте, когда тип приходит из параметра обобщённой функции. Разбор `Map` - типичная задача при работе с конфигами и JSON: ```kotlin inline fun Map.getTyped(key: String): T? = this[key] as? T fun main() { val config = mapOf("port" to 8080, "host" to "localhost", "tls" to true) println(config.getTyped("port")) // 8080 println(config.getTyped("host")) // localhost println(config.getTyped("host")) // null — тип не тот println(config.getTyped("нет")) // null — ключа нет } ``` ## reified и Java-рефлексия ```kotlin inline fun newInstance(): T = T::class.java.getDeclaredConstructor().newInstance() class Service2 { override fun toString() = "сервис создан" } fun main() { val service: Service2 = newInstance() println(service) // сервис создан } ``` `T::class` даёт `KClass`, `T::class.java` - `Class`. Это мост к любому Java API, требующему `Class`: ```kotlin inline fun typeName(): String = T::class.java.name fun main() { println(typeName()) // java.lang.String println(typeName()) // java.lang.Integer — не int! println(typeName>()) // java.util.List println(Int::class.java.name) // int — а так примитив } ``` Две детали из этого примера: - `T::class` знает только стёртый тип: `List` и `List` дадут одинаковый результат. - Параметр типа всегда представлен ссылочным типом, поэтому `T = Int` даёт `java.lang.Integer`, хотя написанный напрямую `Int::class.java` возвращает примитивный `int`. ## Что происходит с inline `reified` - побочный эффект `inline`, а у `inline` есть своя основная задача: убрать объект лямбды. ```kotlin inline fun measure(block: () -> Unit): Long { val start = System.nanoTime() block() return System.nanoTime() - start } fun main() { val time = measure { (1..1000).sum() } println(time > 0) // true } ``` Тело `block` подставляется прямо в `main` - объекта `Function0` не создаётся. Подробнее об этом и о `noinline`/`crossinline`: [Inline-функции](../Функциональный%20Kotlin/Inline-функции.md) Для `reified` важно только одно следствие inline: **функция перестаёт существовать как метод**, поэтому её нельзя вызвать не из Kotlin и нельзя взять на неё ссылку. ## Ограничения reified ```kotlin inline fun demo(): String { val cls = T::class // можно val arr = arrayOfNulls(1) // можно // val instance = T() // нельзя: нет конструктора в типе // val companion = T.Companion // нельзя return "${cls.simpleName} ${arr.size}" } fun main() { println(demo()) // String 1 } ``` | Можно с `reified T` | Нельзя | |---|---| | `value is T`, `value as? T` | `T()` - создать экземпляр | | `T::class`, `T::class.java` | обращаться к статическим членам `T` | | `arrayOfNulls()`, `arrayOf()` | различать `List` и `List` | | передать `T` в другую `reified`-функцию | вызвать из Java | | вывести `T` из типа переменной | передать тип как значение в рантайме | Создание экземпляра требует рефлексии: ```kotlin inline fun createOrNull(): T? = runCatching { T::class.java.getDeclaredConstructor().newInstance() }.getOrNull() class WithDefault { override fun toString() = "создан" } class WithoutDefault(val x: Int) fun main() { println(createOrNull()) // создан println(createOrNull()) // null — нет пустого конструктора } ``` ## Проброс reified дальше ```kotlin inline fun logType(value: Any) { println("${T::class.simpleName}: ${check2(value)}") } inline fun check2(value: Any): Boolean = value is T fun main() { logType("текст") // String: true logType("текст") // Int: false } ``` `reified` передаётся в другие `inline reified`-функции - каждая подстановка сохраняет фактический тип. А вот в обычную обобщённую функцию - нет: ```kotlin fun notReified(value: Any): Boolean = value != null // T здесь стёрт inline fun caller(value: Any): Boolean = notReified(value) fun main() { println(caller("текст")) // true — но проверки типа внутри нет } ``` ## Альтернатива без reified Когда inline невозможен (рекурсия, большая функция, вызов из Java), тип передают явно: ```kotlin fun parseWith(raw: String, type: kotlin.reflect.KClass): T? = when (type) { Int::class -> raw.toIntOrNull() as T? String::class -> raw as T else -> null } fun main() { println(parseWith("42", Int::class)) // 42 println(parseWith("текст", String::class)) // текст } ``` Это ровно Java-подход с `Class`. Он многословнее в вызове, зато функция остаётся обычным методом. > [!info] Отличия от Java > | Java | Kotlin | > |---|---| > | ` T parse(String s, Class type)` | `inline fun parse(s: String): T` | > | `parse(json, User.class)` | `parse(json)` или `val u: User = parse(json)` | > | `x instanceof T` - невозможно | `x is T` с `reified` | > | `new T[]` - запрещён | `arrayOf()` с `reified` | > | `T.class` - невозможно | `T::class` с `reified` | > | нет inline-функций | `inline` + подстановка на этапе компиляции | > > **`reified` устраняет параметр-токен типа.** Это самая заметная разница в API. Jackson в Java: > ```java > User user = mapper.readValue(json, User.class); > List users = mapper.readValue(json, new TypeReference>() {}); > ``` > Kotlin-модуль того же Jackson: > ```kotlin > // val user: User = mapper.readValue(json) > // val users: List = mapper.readValue(json) > ``` > Второй случай особенно показателен: в Java для обобщённого типа приходится создавать анонимный класс `TypeReference`, чтобы тип сохранился в сигнатуре суперкласса - единственное место, где Java не стирает дженерики. Kotlin обходится выводом типа из переменной. > > **Стирание никуда не делось.** `reified` работает только потому, что тип подставляется при компиляции. Поэтому `reified T` **не** даёт настоящей информации об аргументах типа: > ```kotlin > inline fun describeType(): String = T::class.simpleName ?: "?" > > fun main() { > println(describeType>()) // List — параметр потерян > println(describeType>()) // List — то же самое > } > ``` > Для полного типа с аргументами нужен `typeOf()` из `kotlin.reflect`, и он требует зависимости `kotlin-reflect`. > > **Inline-функций в Java нет.** JVM инлайнит методы на уровне JIT в рантайме, но это оптимизация, невидимая системе типов. Kotlin инлайнит на этапе компиляции, что даёт две вещи сразу: отсутствие объекта лямбды и `reified`. Цена - рост размера байт-кода и невозможность вызвать такую функцию из Java. > > **`filterIsInstance` аналога в Java не имеет.** Эквивалент - `stream().filter(X.class::isInstance).map(X.class::cast)`, где класс указывается дважды. > > Сравнить: [Параметрический полиморфизм в Java](../../Программирование%20на%20Java/Дженерики/Параметрический%20полиморфизм.md), [Reflection API в Java](../../Программирование%20на%20Java/Рефлексия/Reflection%20API.md) > [!warning] Подводные камни > **`reified` без `inline` не компилируется** - это не выбор, а требование. Если функция не может быть inline (рекурсивная, очень большая, публичное API для Java), придётся передавать `KClass` вручную. > > **Тип всё равно стёрт внутри аргументов:** > ```kotlin > inline fun isList(value: Any): Boolean = value is T > > fun main() { > println(isList>(listOf(1, 2))) // true — хотя это List! > } > ``` > `is List` в подставленном коде превращается в `is List<*>`. Это молчаливая ошибка: проверка выглядит строгой, но не различает аргументы типа. > > **`as T` внутри reified-функции не всегда проверяется:** > ```kotlin > inline fun forceCast(value: Any): T = value as T > > fun main() { > val list: List = forceCast(listOf(1, 2)) // прошло! > println(list.size) // 2 > try { > println(list[0].length) > } catch (e: ClassCastException) { > println("упало только при обращении к элементу") > } > } > ``` > Каст к `List` сводится к касту к `List`, который успешен. Ошибка всплывает позже. > > **Inline раздувает байт-код.** Тело функции копируется в каждое место вызова. Для большой функции, вызываемой из сотни мест, это заметный рост размера класса - и потеря выигрыша от кеша инструкций процессора. `reified` оправдывает inline только для коротких функций. > > **Inline-функцию нельзя вызвать из Java:** > ```kotlin > inline fun kotlinOnly(): String = T::class.simpleName ?: "?" > > fun main() { println(kotlinOnly()) } // Int > ``` > Java-код не умеет подставлять тело и не знает про `reified`. Для API, доступного из Java, нужна обычная функция с `Class`. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md) > > **`T()` не работает** - у параметра типа нет конструктора. Варианты: передать фабрику `() -> T` параметром или использовать рефлексию, которая упадёт при отсутствии пустого конструктора. > > **Ссылку на inline-функцию взять нельзя:** > ```kotlin > inline fun typeNameOf(): String = T::class.simpleName ?: "?" > > fun main() { > // val ref = ::typeNameOf // ошибка: нельзя взять ссылку > println(typeNameOf()) // Int > } > ``` > > **`reified` требует, чтобы тип был известен в месте вызова.** Передать тип из переменной нельзя: > ```kotlin > inline fun show2(value: Any) = println(value is T) > > fun wrapper(value: Any) { > // show2(value) // ошибка: U не reified > show2(value) > } > > fun main() { wrapper("x") } // true > ``` > Цепочка `reified` обрывается на первой обычной обобщённой функции. > > **`T::class.java` и `Int::class.java` дают разные классы:** > ```kotlin > inline fun jvmName(): String = T::class.java.name > > fun main() { > println(jvmName()) // java.lang.Integer > println(Int::class.java.name) // int > println(Int::class.javaObjectType.name) // java.lang.Integer > } > ``` > Через параметр типа всегда получается обёртка - примитив не может быть аргументом типа. Написанный напрямую `Int::class.java` даёт примитивный `int`, и если результат передаётся в Java-API, различие имеет значение: `Integer.class` и `int.class` - разные объекты, и, например, `getDeclaredMethod` по ним найдёт разные методы. Явные формы - `javaPrimitiveType` и `javaObjectType`. Подробнее: [Платформенные типы](../Синтаксис%20и%20типы/Null-безопасность/Платформенные%20типы.md) > > **Изменение тела inline-функции требует перекомпиляции всех вызывающих.** Если функция опубликована в библиотеке, клиенты, собранные со старой версией, продолжат исполнять старое тело - оно физически скопировано в их байт-код. Для публичных API это аргумент против inline. Далее: [Функции высшего порядка](../Функциональный%20Kotlin/Функции%20высшего%20порядка.md)