Метапрограммирование в Kotlin: от рефлексии до кодогенерации

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Метапрограммирование в Kotlin: как заставить код писать код

Представьте, что вы пишете код, который сам пишет код. Звучит как научная фантастика? В мире метапрограммирования в Kotlin это реальность. Метапрограммирование — это техника, при которой программа манипулирует собой или другими программами как данными. Для Kotlin-разработчика это суперсила, позволяющая сократить тонны шаблонного кода, создавать гибкие фреймворки и автоматизировать рутину.

В этой статье мы разберем продвинутые техники метапрограммирования в Kotlin: от классической рефлексии до современной кодогенерации с KSP. Вы узнаете, как использовать реифицированные типы, inline-функции, аннотации и многое другое. Поехали!

Рефлексия в Kotlin: заглядываем под капот

Рефлексия — это фундамент метапрограммирования. В Kotlin она реализована через библиотеку kotlin-reflect. С её помощью можно динамически получать информацию о классах, функциях и свойствах во время выполнения.

Пример: допустим, вам нужно сериализовать объект в JSON, не зная его тип заранее. Рефлексия позволяет это сделать:

import kotlin.reflect.full.memberProperties

fun toJson(obj: Any): String {
    val sb = StringBuilder("{")
    val props = obj::class.memberProperties
    props.forEachIndexed { index, prop ->
        sb.append("\"${prop.name}\": \"${prop.get(obj)}\"")
        if (index < props.size - 1) sb.append(", ")
    }
    sb.append("}")
    return sb.toString()
}

data class User(val name: String, val age: Int)

fun main() {
    val user = User("Алиса", 30)
    println(toJson(user)) // {"name": "Алиса", "age": "30"}
}

Это базовая, но мощная техника. Однако рефлексия имеет недостаток — производительность. Для высоконагруженных систем лучше использовать кодогенерацию, о которой мы поговорим позже.

Inline-функции и реифицированные типы

Одна из самых крутых возможностей Kotlin для метапрограммирования — это реифицированные (reified) типы. Они работают только внутри inline-функций и позволяют сохранить информацию о типе во время выполнения, что обычно невозможно из-за стирания типов в JVM.

Посмотрите, как элегантно можно получить тип на этапе выполнения:

inline fun <reified T> typeName(): String {
    return T::class.simpleName ?: "Unknown"
}

fun main() {
    println(typeName<String>())   // String
    println(typeName<List<Int>>()) // List
}

Без reified такой фокус не пройдёт — компилятор просто сотрёт тип. А с помощью этой техники вы можете создавать типизированные фабрики, сериализаторы и даже целые фреймворки. Это настоящий реифицированный тип в Kotlin в действии.

Аннотации: ваш личный DSL для метаданных

Аннотации в Kotlin — это не просто маркеры. В сочетании с рефлексией или кодогенерацией они превращаются в мощный инструмент метапрограммирования. Создадим свою аннотацию для валидации:

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class NotBlank(val message: String = "Поле не должно быть пустым")

fun validate(obj: Any): List<String> {
    val errors = mutableListOf<String>()
    obj::class.memberProperties.forEach { prop ->
        val annotation = prop.findAnnotation<NotBlank>()
        if (annotation != null) {
            val value = prop.get(obj) as? String
            if (value.isNullOrBlank()) {
                errors.add("${prop.name}: ${annotation.message}")
            }
        }
    }
    return errors
}

data class Form(val name: String, @NotBlank val email: String)

fun main() {
    val form = Form("Иван", "")
    println(validate(form)) // [email: Поле не должно быть пустым]
}

Такой подход позволяет отделить логику валидации от бизнес-логики. А если подключить кодогенерацию, то проверки будут выполняться на этапе компиляции, без рефлексии.

Kotlin Symbol Processing (KSP): кодогенерация нового поколения

Если вам нужно генерировать код на этапе компиляции, забудьте о старом KAPT. KSP (Kotlin Symbol Processing) — это современный инструмент, который работает напрямую с Kotlin-синтаксисом, а не с Java-байткодом. Он быстрее, проще и идеально подходит для кодогенерации в Kotlin.

Допустим, мы хотим генерировать фабрики для классов с аннотацией @Factory. Создадим простой процессор:

// build.gradle.kts
plugins {
    id("com.google.devtools.ksp") version "1.9.0-1.0.13"
}

// Файл: FactoryProcessor.kt
class FactoryProcessor : SymbolProcessor {
    override fun process(resolver: Resolver): List<KSAnnotated> {
        val symbols = resolver
            .getSymbolsWithAnnotation("com.example.Factory")
            .filterIsInstance<KSClassDeclaration>()
        
        symbols.forEach { classDecl ->
            val file = classDecl.containingFile!!
            val packageName = file.packageName.asString()
            val className = classDecl.simpleName.asString()
            
            val factoryCode = """
                package ${packageName}
                
                object ${className}Factory {
                    fun create(): $className = ${className}()
                }
            """.trimIndent()
            
            file.packageName?.let {
                // Здесь код для записи файла через CodeGenerator
            }
        }
        return emptyList()
    }
}

KSP автоматически создаст файлы UserFactory.kt для всех классов, помеченных @Factory. Это идеально для DI-контейнеров, мапперов и генерации API-клиентов.

Delegated Properties: метапрограммирование без магии

Делегированные свойства в Kotlin — это ещё одна форма метапрограммирования. Вы можете перехватывать чтение и запись свойств, добавлять ленивую инициализацию, кэширование или даже валидацию.

Создадим свой делегат, который логирует доступ к свойству:

import kotlin.reflect.KProperty

class LoggingDelegate<T>(private val initialValue: T) {
    private var value = initialValue
    
    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        println("Чтение ${property.name} = $value")
        return value
    }
    
    operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
        println("Запись ${property.name}: $value -> $newValue")
        value = newValue
    }
}

class User {
    var name: String by LoggingDelegate("Гость")
}

fun main() {
    val user = User()
    println(user.name) // Чтение name = Гость
    user.name = "Алиса" // Запись name: Гость -> Алиса
}

Это мощный инструмент для создания DSL, ORM-подобных систем и фреймворков

Блоги

Рекомендаций по книгам