Métaprogrammation en Kotlin : de la réflexion à la génération de code

Formateur Python en ligne pour débutants

Apprenez Python facilement sans surcharge théorique. Résolvez des tâches pratiques avec vérification automatique et écrivez du code directement dans le navigateur.

Start Course

Métaprogrammation en Kotlin : comment faire en sorte que le code écrive du code

\n\n

Imaginez que vous écrivez un code qui écrit lui-même du code. Cela ressemble à de la science-fiction ? Dans le monde de la métaprogrammation en Kotlin, c'est une réalité. La métaprogrammation est une technique par laquelle un programme se manipule lui-même ou manipule d'autres programmes en tant que données. Pour un développeur Kotlin, c'est un super-pouvoir qui permet de réduire des tonnes de code passe-partout, de créer des frameworks flexibles et d'automatiser les tâches répétitives.

\n\n

Dans cet article, nous allons explorer les techniques avancées de métaprogrammation en Kotlin : de la réflexion classique à la génération de code moderne avec KSP. Vous apprendrez à utiliser les types réifiés, les fonctions inline, les annotations et bien plus encore. C'est parti !

\n\n

Réflexion en Kotlin : regarder sous le capot

\n\n

La réflexion est le fondement de la métaprogrammation. En Kotlin, elle est implémentée via la bibliothèque kotlin-reflect. Elle permet d'obtenir dynamiquement des informations sur les classes, les fonctions et les propriétés pendant l'exécution.

\n\n

Exemple : supposons que vous deviez sérialiser un objet en JSON sans connaître son type à l'avance. La réflexion le permet :

\n\n
import kotlin.reflect.full.memberProperties\n\nfun toJson(obj: Any): String {\n    val sb = StringBuilder("{")\n    val props = obj::class.memberProperties\n    props.forEachIndexed { index, prop ->\n        sb.append("\\"${prop.name}\\": \\"${prop.get(obj)}\\"")\n        if (index < props.size - 1) sb.append(", ")\n    }\n    sb.append("}")\n    return sb.toString()\n}\n\ndata class User(val name: String, val age: Int)\n\nfun main() {\n    val user = User("Alice", 30)\n    println(toJson(user)) // {"name": "Alice", "age": "30"}\n}\n
\n\n

C'est une technique basique mais puissante. Cependant, la réflexion a un inconvénient : la performance. Pour les systèmes à forte charge, il est préférable d'utiliser la génération de code, dont nous parlerons plus tard.

\n\n

Fonctions inline et types réifiés

\n\n

L'une des fonctionnalités les plus intéressantes de Kotlin pour la métaprogrammation est les types réifiés (reified). Ils fonctionnent uniquement à l'intérieur des fonctions inline et permettent de conserver les informations de type pendant l'exécution, ce qui est généralement impossible en raison de l'effacement des types sur la JVM.

\n\n

Regardez comme il est élégant d'obtenir le type à l'exécution :

\n\n
inline fun <reified T> typeName(): String {\n    return T::class.simpleName ?: "Inconnu"\n}\n\nfun main() {\n    println(typeName<String>())   // String\n    println(typeName<List<Int>>()) // List\n}\n
\n\n

Sans reified, cette astuce ne fonctionnerait pas — le compilateur effacerait simplement le type. Grâce à cette technique, vous pouvez créer des fabriques typées, des sérialiseurs et même des frameworks entiers. C'est le type réifié en Kotlin en action.

\n\n

Annotations : votre DSL personnel pour les métadonnées

\n\n

Les annotations en Kotlin ne sont pas de simples marqueurs. Combinées à la réflexion ou à la génération de code, elles deviennent un outil puissant de métaprogrammation. Créons notre propre annotation pour la validation :

\n\n
@Target(AnnotationTarget.FIELD)\n@Retention(AnnotationRetention.RUNTIME)\nannotation class NotBlank(val message: String = "Le champ ne doit pas être vide")\n\nfun validate(obj: Any): List<String> {\n    val errors = mutableListOf<String>()\n    obj::class.memberProperties.forEach { prop ->\n        val annotation = prop.findAnnotation<NotBlank>()\n        if (annotation != null) {\n            val value = prop.get(obj) as? String\n            if (value.isNullOrBlank()) {\n                errors.add("${prop.name}: ${annotation.message}")\n            }\n        }\n    }\n    return errors\n}\n\ndata class Form(val name: String, @NotBlank val email: String)\n\nfun main() {\n    val form = Form("Jean", "")\n    println(validate(form)) // [email: Le champ ne doit pas être vide]\n}\n                

Blogs

Book Recommendations