Avatar ·

Как получить класс дженерик-параметра в Kotlin? (reified, inline и TypeReference)

Всем привет! Я пишу на Kotlin недавно, и столкнулся с проблемой, которая в Java решалась просто, а в Kotlin вызывает у меня трудности. Мне нужно внутри функции узнать, с каким типом параметра она была вызвана. Например, у меня есть такой код: ```kotlin fun processList(items: List) { // Как здесь узнать, что T — это String, Int или что-то ещё? // Хочу вывести тип в лог println("Processing items of type: ${T::class}") // Ошибка! } ``` В Java я бы использовал Class и передавал его явно. Но в Kotlin я хочу избежать этого и сделать API чище. Пробовал использовать T::class, но компилятор говорит: "Cannot use 'T' as reified type parameter. Use a class instead." Я читал про reified, но не до конца понял, как это работает. Плюс, мне нужно, чтобы функция была универсальной и работала с любыми типами, включая такие, где дженерики стираются (например, List). Как правильно получить класс T в Kotlin? Заранее спасибо! <<>>

Решение с помощью reified и inline-функций

В Kotlin для получения класса дженерик-параметра на этапе выполнения используется ключевое слово reified в сочетании с inline. Это позволяет "материализовать" тип, который в Java стирается. Без reified информация о T недоступна во время выполнения из-за стирания типов (type erasure).

Базовый пример

Объявите функцию как inline и добавьте reified перед параметром типа:

inline fun <reified T> processList(items: List<T>) {
    println("Processing items of type: ${T::class.simpleName}")
}

// Использование:
processList(listOf("a", "b")) // Выведет: Processing items of type: String
processList(listOf(1, 2, 3))  // Выведет: Processing items of type: Int

Теперь T::class работает, потому что на месте вызова компилятор подставляет реальный тип (благодаря инлайнингу).

Получение полного дженерик-типа (например, List<String>)

Простой T::class даст только сырой класс (например, List::class), но не параметр. Чтобы получить полный тип с аргументами, используйте typeOf<T>() (доступно с Kotlin 1.3.40+):

import kotlin.reflect.typeOf

inline fun <reified T> printFullType(value: T) {
    val type = typeOf<T>()
    println("Full type: $type")
}

// Пример:
printFullType(listOf("hello")) // Full type: kotlin.collections.List<kotlin.String>
printFullType(mapOf(1 to "a")) // Full type: kotlin.collections.Map<kotlin.Int, kotlin.String>

Когда reified не подходит (например, в классах или интерфейсах)

reified работает только в inline-функциях и не может быть использован в обычных классах, свойствах или абстрактных методах. В таких случаях применяйте паттерн TypeReference или передавайте Class явно:

abstract class TypeReference<T> {
    val type: java.lang.reflect.Type
        get() = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0]
}

// Использование:
class MyProcessor : TypeReference<List<String>>() {
    fun process() {
        println("Type: $type") // Выведет: java.util.List<java.lang.String>
    }
}

Работа с дженериками в рантайме через Gson/Moshi

Библиотеки сериализации часто используют этот паттерн. Например, в Gson:

import com.google.gson.reflect.TypeToken

val type = object : TypeToken<List<String>>() {}.type
val list: List<String> = Gson().fromJson(jsonString, type)

Важные замечания

  • Производительность: inline увеличивает размер байткода, но ускоряет выполнение. Используйте только для небольших функций.
  • Ограничения: reified нельзя использовать в функциях, которые не являются inline, или внутри лямбд, не помеченных как inline.
  • Альтернатива для Java-совместимости: Если вы пишете API для Java, лучше передавать Class<T> явно.

Таким образом, для большинства сценариев используйте inline fun <reified T> — это идиоматичный способ получить класс дженерик-параметра в Kotlin. Если нужно сохранить полную информацию о вложенных дженериках, применяйте typeOf<T>() или TypeReference.

<<>>

Альтернативный подход: передача Class параметра вручную

Если вы не можете использовать reified (например, функция не может быть inline из-за архитектурных ограничений), вернитесь к классическому Java-подходу — передавайте Class<T> как параметр:

fun <T> processList(items: List<T>, clazz: Class<T>) {
    println("Type: ${clazz.simpleName}")
}

// Использование:
processList(listOf("a"), String::class.java)
processList(listOf(1), Int::class.java)

Этот метод совместим с Java-кодом и не требует инлайнинга. Недостаток — загромождает сигнатуру функции и требует от вызывающего кода явно указывать тип.

Использование Any? и is-проверок

Если вам нужно просто проверить тип элементов в коллекции (а не получить сам параметр), можно обойтись без дженериков:

fun processItems(items: List<*>) {
    items.firstOrNull()?.let { first ->
        when (first) {
            is String -> println("List of Strings")
            is Int -> println("List of Ints")
            else -> println("Unknown type")
        }
    }
}

Это не даст вам класс T, но решит задачу проверки типа во время выполнения. Однако помните, что пустой список (listOf()) не позволит определить тип.