Files
obsidian/Kotlin/Дженерики/Reified и inline.md
T

360 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Reified и inline
description: Как reified обходит стирание типов, что компилятор делает с inline-функциями и чего reified не умеет
tags:
- kotlin
- generics
---
На JVM параметры типа стираются: в рантайме `List<Int>` и `List<String>` - один класс. Но если функция объявлена `inline`, компилятор подставляет её тело в место вызова, где фактический тип **известен**. Это и позволяет `reified`.
```kotlin
inline fun <reified T> isOfType(value: Any): Boolean = value is T
fun main() {
println(isOfType<String>("текст")) // true
println(isOfType<Int>("текст")) // false
println(isOfType<List<*>>(listOf(1))) // true
}
```
Без `reified` строка `value is T` не компилируется - проверить стёртый тип невозможно.
## Что делает компилятор
```kotlin
inline fun <reified T> nameOf(): String = T::class.simpleName ?: "?"
fun main() {
println(nameOf<String>()) // String
println(nameOf<Int>()) // Int
}
```
В байт-коде вызова `nameOf<String>()` не остаётся: компилятор подставляет тело и заменяет `T` на `String`, получая буквально `String::class.simpleName`. Поэтому `reified` и требует `inline` - без подстановки подставлять нечего.
Следствие: `reified`-функцию нельзя вызвать из Java и нельзя передать `T` динамически.
## Главный случай: парсинг и десериализация
Задача, ради которой `reified` чаще всего и используют:
```kotlin
inline fun <reified T> 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<Int>("42")) // 42
println(parseAs<Int>("abc")) // null
println(parseAs<Double>("1.5")) // 1.5
println(parseAs<Boolean>("true")) // true
println(parseAs<String>("текст")) // текст
}
```
В Java такая функция потребовала бы параметр `Class<T> type` в сигнатуре. Именно это делают Jackson (`readValue(json, User.class)`) и Gson (`fromJson(json, User.class)`); Kotlin-обёртки над ними объявляют `inline fun <reified T> readValue(json: String): T`, и тип выводится из контекста:
```kotlin
// С kotlinx.serialization или Jackson Kotlin module:
// val user: User = mapper.readValue(json) // тип берётся из объявления переменной
```
## filterIsInstance и прочее в stdlib
```kotlin
fun main() {
val mixed: List<Any> = listOf(1, "a", 2.0, 3, "b")
println(mixed.filterIsInstance<Int>()) // [1, 3]
println(mixed.filterIsInstance<String>()) // [a, b]
println(mixed.filterIsInstance<Number>()) // [1, 2.0, 3]
val target = mutableListOf<Int>()
mixed.filterIsInstanceTo(target)
println(target) // [1, 3]
}
```
`filterIsInstance` объявлена как `inline fun <reified R> Iterable<*>.filterIsInstance(): List<R>` - без `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<String?> = arrayOfNulls(2)
println(typed.size) // 2
println(emptyArray<Int>().size) // 0
}
```
`toTypedArray` без `reified` не смогла бы создать массив правильного типа - у `Array<T>` в рантайме есть настоящий тип элементов.
## Безопасное приведение
```kotlin
inline fun <reified T> Any?.asOrNull(): T? = this as? T
fun main() {
val value: Any = "текст"
println(value.asOrNull<String>()?.length) // 5
println(value.asOrNull<Int>()) // null — без исключения
}
```
Это удобнее, чем `as? String` в каждом месте, когда тип приходит из параметра обобщённой функции.
Разбор `Map<String, Any>` - типичная задача при работе с конфигами и JSON:
```kotlin
inline fun <reified T> Map<String, Any>.getTyped(key: String): T? = this[key] as? T
fun main() {
val config = mapOf<String, Any>("port" to 8080, "host" to "localhost", "tls" to true)
println(config.getTyped<Int>("port")) // 8080
println(config.getTyped<String>("host")) // localhost
println(config.getTyped<Int>("host")) // null — тип не тот
println(config.getTyped<Int>("нет")) // null — ключа нет
}
```
## reified и Java-рефлексия
```kotlin
inline fun <reified T : Any> newInstance(): T = T::class.java.getDeclaredConstructor().newInstance()
class Service2 {
override fun toString() = "сервис создан"
}
fun main() {
val service: Service2 = newInstance()
println(service) // сервис создан
}
```
`T::class` даёт `KClass<T>`, `T::class.java` - `Class<T>`. Это мост к любому Java API, требующему `Class`:
```kotlin
inline fun <reified T> typeName(): String = T::class.java.name
fun main() {
println(typeName<String>()) // java.lang.String
println(typeName<Int>()) // java.lang.Integer — не int!
println(typeName<List<String>>()) // java.util.List
println(Int::class.java.name) // int — а так примитив
}
```
Две детали из этого примера:
- `T::class` знает только стёртый тип: `List<String>` и `List<Int>` дадут одинаковый результат.
- Параметр типа всегда представлен ссылочным типом, поэтому `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 <reified T> demo(): String {
val cls = T::class // можно
val arr = arrayOfNulls<T>(1) // можно
// val instance = T() // нельзя: нет конструктора в типе
// val companion = T.Companion // нельзя
return "${cls.simpleName} ${arr.size}"
}
fun main() {
println(demo<String>()) // String 1
}
```
| Можно с `reified T` | Нельзя |
|---|---|
| `value is T`, `value as? T` | `T()` - создать экземпляр |
| `T::class`, `T::class.java` | обращаться к статическим членам `T` |
| `arrayOfNulls<T>()`, `arrayOf<T>()` | различать `List<Int>` и `List<String>` |
| передать `T` в другую `reified`-функцию | вызвать из Java |
| вывести `T` из типа переменной | передать тип как значение в рантайме |
Создание экземпляра требует рефлексии:
```kotlin
inline fun <reified T : Any> createOrNull(): T? = runCatching {
T::class.java.getDeclaredConstructor().newInstance()
}.getOrNull()
class WithDefault { override fun toString() = "создан" }
class WithoutDefault(val x: Int)
fun main() {
println(createOrNull<WithDefault>()) // создан
println(createOrNull<WithoutDefault>()) // null — нет пустого конструктора
}
```
## Проброс reified дальше
```kotlin
inline fun <reified T> logType(value: Any) {
println("${T::class.simpleName}: ${check2<T>(value)}")
}
inline fun <reified T> check2(value: Any): Boolean = value is T
fun main() {
logType<String>("текст") // String: true
logType<Int>("текст") // Int: false
}
```
`reified` передаётся в другие `inline reified`-функции - каждая подстановка сохраняет фактический тип. А вот в обычную обобщённую функцию - нет:
```kotlin
fun <T> notReified(value: Any): Boolean = value != null // T здесь стёрт
inline fun <reified T> caller(value: Any): Boolean = notReified<T>(value)
fun main() {
println(caller<String>("текст")) // true — но проверки типа внутри нет
}
```
## Альтернатива без reified
Когда inline невозможен (рекурсия, большая функция, вызов из Java), тип передают явно:
```kotlin
fun <T : Any> parseWith(raw: String, type: kotlin.reflect.KClass<T>): 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<T>`. Он многословнее в вызове, зато функция остаётся обычным методом.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `<T> T parse(String s, Class<T> type)` | `inline fun <reified T> parse(s: String): T` |
> | `parse(json, User.class)` | `parse<User>(json)` или `val u: User = parse(json)` |
> | `x instanceof T` - невозможно | `x is T` с `reified` |
> | `new T[]` - запрещён | `arrayOf<T>()` с `reified` |
> | `T.class` - невозможно | `T::class` с `reified` |
> | нет inline-функций | `inline` + подстановка на этапе компиляции |
>
> **`reified` устраняет параметр-токен типа.** Это самая заметная разница в API. Jackson в Java:
> ```java
> User user = mapper.readValue(json, User.class);
> List<User> users = mapper.readValue(json, new TypeReference<List<User>>() {});
> ```
> Kotlin-модуль того же Jackson:
> ```kotlin
> // val user: User = mapper.readValue(json)
> // val users: List<User> = mapper.readValue(json)
> ```
> Второй случай особенно показателен: в Java для обобщённого типа приходится создавать анонимный класс `TypeReference`, чтобы тип сохранился в сигнатуре суперкласса - единственное место, где Java не стирает дженерики. Kotlin обходится выводом типа из переменной.
>
> **Стирание никуда не делось.** `reified` работает только потому, что тип подставляется при компиляции. Поэтому `reified T` **не** даёт настоящей информации об аргументах типа:
> ```kotlin
> inline fun <reified T> describeType(): String = T::class.simpleName ?: "?"
>
> fun main() {
> println(describeType<List<String>>()) // List — параметр потерян
> println(describeType<List<Int>>()) // List — то же самое
> }
> ```
> Для полного типа с аргументами нужен `typeOf<T>()` из `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<T>` вручную.
>
> **Тип всё равно стёрт внутри аргументов:**
> ```kotlin
> inline fun <reified T> isList(value: Any): Boolean = value is T
>
> fun main() {
> println(isList<List<String>>(listOf(1, 2))) // true — хотя это List<Int>!
> }
> ```
> `is List<String>` в подставленном коде превращается в `is List<*>`. Это молчаливая ошибка: проверка выглядит строгой, но не различает аргументы типа.
>
> **`as T` внутри reified-функции не всегда проверяется:**
> ```kotlin
> inline fun <reified T> forceCast(value: Any): T = value as T
>
> fun main() {
> val list: List<String> = forceCast(listOf(1, 2)) // прошло!
> println(list.size) // 2
> try {
> println(list[0].length)
> } catch (e: ClassCastException) {
> println("упало только при обращении к элементу")
> }
> }
> ```
> Каст к `List<String>` сводится к касту к `List`, который успешен. Ошибка всплывает позже.
>
> **Inline раздувает байт-код.** Тело функции копируется в каждое место вызова. Для большой функции, вызываемой из сотни мест, это заметный рост размера класса - и потеря выигрыша от кеша инструкций процессора. `reified` оправдывает inline только для коротких функций.
>
> **Inline-функцию нельзя вызвать из Java:**
> ```kotlin
> inline fun <reified T> kotlinOnly(): String = T::class.simpleName ?: "?"
>
> fun main() { println(kotlinOnly<Int>()) } // Int
> ```
> Java-код не умеет подставлять тело и не знает про `reified`. Для API, доступного из Java, нужна обычная функция с `Class<T>`. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
>
> **`T()` не работает** - у параметра типа нет конструктора. Варианты: передать фабрику `() -> T` параметром или использовать рефлексию, которая упадёт при отсутствии пустого конструктора.
>
> **Ссылку на inline-функцию взять нельзя:**
> ```kotlin
> inline fun <reified T> typeNameOf(): String = T::class.simpleName ?: "?"
>
> fun main() {
> // val ref = ::typeNameOf // ошибка: нельзя взять ссылку
> println(typeNameOf<Int>()) // Int
> }
> ```
>
> **`reified` требует, чтобы тип был известен в месте вызова.** Передать тип из переменной нельзя:
> ```kotlin
> inline fun <reified T> show2(value: Any) = println(value is T)
>
> fun <U> wrapper(value: Any) {
> // show2<U>(value) // ошибка: U не reified
> show2<Any>(value)
> }
>
> fun main() { wrapper<String>("x") } // true
> ```
> Цепочка `reified` обрывается на первой обычной обобщённой функции.
>
> **`T::class.java` и `Int::class.java` дают разные классы:**
> ```kotlin
> inline fun <reified T> jvmName(): String = T::class.java.name
>
> fun main() {
> println(jvmName<Int>()) // 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)