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

19 KiB
Raw Blame History

title, description, tags
title description tags
Reified и inline Как reified обходит стирание типов, что компилятор делает с inline-функциями и чего reified не умеет
kotlin
generics

На JVM параметры типа стираются: в рантайме List<Int> и List<String> - один класс. Но если функция объявлена inline, компилятор подставляет её тело в место вызова, где фактический тип известен. Это и позволяет reified.

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 не компилируется - проверить стёртый тип невозможно.

Что делает компилятор

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 чаще всего и используют:

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, и тип выводится из контекста:

// С kotlinx.serialization или Jackson Kotlin module:
// val user: User = mapper.readValue(json)      // тип берётся из объявления переменной

filterIsInstance и прочее в stdlib

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:

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> в рантайме есть настоящий тип элементов.

Безопасное приведение

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:

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-рефлексия

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:

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 есть своя основная задача: убрать объект лямбды.

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-функции

Для reified важно только одно следствие inline: функция перестаёт существовать как метод, поэтому её нельзя вызвать не из Kotlin и нельзя взять на неё ссылку.

Ограничения reified

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 из типа переменной передать тип как значение в рантайме

Создание экземпляра требует рефлексии:

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 дальше

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-функции - каждая подстановка сохраняет фактический тип. А вот в обычную обобщённую функцию - нет:

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), тип передают явно:

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:

User user = mapper.readValue(json, User.class);
List<User> users = mapper.readValue(json, new TypeReference<List<User>>() {});

Kotlin-модуль того же Jackson:

// val user: User = mapper.readValue(json)
// val users: List<User> = mapper.readValue(json)

Второй случай особенно показателен: в Java для обобщённого типа приходится создавать анонимный класс TypeReference, чтобы тип сохранился в сигнатуре суперкласса - единственное место, где Java не стирает дженерики. Kotlin обходится выводом типа из переменной.

Стирание никуда не делось. reified работает только потому, что тип подставляется при компиляции. Поэтому reified T не даёт настоящей информации об аргументах типа:

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, Reflection API в Java

[!warning] Подводные камни reified без inline не компилируется - это не выбор, а требование. Если функция не может быть inline (рекурсивная, очень большая, публичное API для Java), придётся передавать KClass<T> вручную.

Тип всё равно стёрт внутри аргументов:

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-функции не всегда проверяется:

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:

inline fun <reified T> kotlinOnly(): String = T::class.simpleName ?: "?"

fun main() { println(kotlinOnly<Int>()) }      // Int

Java-код не умеет подставлять тело и не знает про reified. Для API, доступного из Java, нужна обычная функция с Class<T>. Подробнее: Kotlin из Java

T() не работает - у параметра типа нет конструктора. Варианты: передать фабрику () -> T параметром или использовать рефлексию, которая упадёт при отсутствии пустого конструктора.

Ссылку на inline-функцию взять нельзя:

inline fun <reified T> typeNameOf(): String = T::class.simpleName ?: "?"

fun main() {
    // val ref = ::typeNameOf        // ошибка: нельзя взять ссылку
    println(typeNameOf<Int>())        // Int
}

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

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 дают разные классы:

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. Подробнее: Платформенные типы

Изменение тела inline-функции требует перекомпиляции всех вызывающих. Если функция опубликована в библиотеке, клиенты, собранные со старой версией, продолжат исполнять старое тело - оно физически скопировано в их байт-код. Для публичных API это аргумент против inline.

Далее: Функции высшего порядка