Метапрограммирование в 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-подобных систем и фреймворков