360 lines
19 KiB
Markdown
360 lines
19 KiB
Markdown
---
|
||
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)
|