19 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| Reified и inline | Как reified обходит стирание типов, что компилятор делает с inline-функциями и чего reified не умеет |
|
На 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): Tparse(json, User.class)parse<User>(json)илиval u: User = parse(json)x instanceof T- невозможноx is Tсreifiednew T[]- запрещёнarrayOf<T>()сreifiedT.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>()) } // IntJava-код не умеет подставлять тело и не знает про
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.
Далее: Функции высшего порядка