L’introspection et la réification de type
Qu’est-ce que l’introspection ?
L’introspection (ou réflexivité) est un concept lié aux objets, qui permet de les créer et/ou de les gérer dynamiquement, de découvrir les attributs ou les méthodes des classes, ou encore de changer la visibilité d’attributs ou de méthodes.
Utiliser l’introspection dans les projets
Tout ce qui tourne autour de l’introspection n’est pas inclus dans le langage Kotlin dans le but de réduire la taille des programmes. Aussi, pour pouvoir utiliser le concept d’introspection, il convient d’ajouter une dépendance tierce dans le fichier build.gradle.kts du projet IntelliJ IDEA. Quand on l’ouvre, ce fichier ressemble pour le moment à ceci :
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm") version "1.4.10"
application
}
group = "me.rolan"
version = "1.0-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
testImplementation(kotlin("test-junit"))
}
tasks.withType<KotlinCompile>() {
kotlinOptions.jvmTarget = "1.8" }
application {
mainClassName = "MainKt"
}
Dans la section dependencies, il faut ajouter la ligne suivante afin de pouvoir utiliser l’introspection en tant que dépendance tierce dans le projet :...
Découvrir une classe par introspection
En guise d’exemple, nous allons découvrir par introspection une classe simple du langage Kotlin, souvent manipulée lorsqu’on écrit un programme : la classe String.
Afin de pouvoir utiliser l’introspection, la première étape consiste à récupérer une interface KClass. C’est à partir d’une instance de cette interface que nous pourrons ensuite, dynamiquement, explorer le contenu de la classe String.
Récupérer une instance de l’interface KClass se fait soit à partir du nom de la classe à inspecter, soit à partir d’une instance de la classe.
Dans les deux cas, la syntaxe est assez simple. Pour récupérer une instance de l’interface KClass à partir du nom de la classe à inspecter, il convient de préfixer le nom de la classe par ::class. Pour récupérer une instance de l’interface KClass depuis une instance de la classe, il convient d’appeler sur l’instance tout d’abord l’attribut javaClass, qui renvoie un objet Class, puis sur cet objet, l’attribut kotlin.
C’est quoi cet objet Class ?
L’objet Class appartient au monde Java tandis que l’objet KClass appartient au monde Kotlin. Dans les deux cas, il s’agit d’une représentation de la classe. C’est à partir de cette représentation que la machine virtuelle Java (JVM) récupère toutes les caractéristiques des classes pour les manipuler. Et c’est exactement ce que nous allons faire.
Il est possible de récupérer une instance de l’interface KClass à partir d’un objet Class en appelant l’attribut kotlin. L’inverse est également possible. Pour récupérer un objet Class à partir d’une instance de l’interface KClass, il convient d’utiliser l’attribut java.
Le programme suivant récupère une instance de l’interface KClass de la classe String via les deux méthodes décrites précédemment :
fun main()
{
//KClass from the class name
val kClass = String::class
//KClass from class instance
val otherKClass = String().javaClass.kotlin
}
Maintenant...
Instancier dynamiquement des objets
L’introspection permet non seulement de découvrir dynamiquement les différents éléments qui entrent dans la composition d’une classe (classe mère, interfaces implémentées, constructeurs, méthodes, attributs), mais aussi de créer des instances d’objets dynamiquement, sans appeler le constructeur comme nous l’avons fait jusqu’à maintenant.
Reprenons l’exemple composé des classes Labrador, Dog et de l’interface Animal :
interface Animal
abstract class Dog(val name: String, protected var weight: Int)
: Animal
{
abstract fun bark()
fun walk()
{
weight--
}
fun eat(foodWeight: Int): Boolean
{
return if (foodWeight > weight)
{
false
}
else
{
weight++
true
}
}
private fun test()
{
}
}
class Labrador(val color: String, private var age: Int, name:
String, weight: Int)
: Dog(name, weight)
{
constructor(name: String) : this("Black", 1, name, 5)
override fun bark()
{
println("waf! Waf!")
}
override fun toString(): String
{ ...
Aller plus loin
Si l’introspection permet de découvrir les éléments qui composent une classe ou encore d’instancier un objet de manière dynamique, on se rend bien compte qu’elle ne fait que complexifier des opérations que l’on sait faire de manière bien plus simple.
Mais alors, à quoi sert donc l’introspection ?
Bien souvent, l’introspection permet de manipuler des objets, des méthodes ou des attributs qui sont inaccessibles…
Quand on crée un logiciel, il n’est pas rare d’utiliser des bibliothèques tierces développées par d’autres développeurs. Il convient alors de se plier aux règles fixées par les auteurs de ces bibliothèques. Ceux-ci peuvent avoir défini des classes ou des attributs comme privés par exemple. Il devient alors compliqué d’y accéder.
Imaginons que les classes Labrador et Dog et l’interface Animal sont "packagées" dans une bibliothèque tierce. Nous souhaitons modifier l’âge d’un chien en dehors de son constructeur. Ce n’est malheureusement pas possible, car l’attribut age est privé et aucun mutateur n’est disponible. Puisque la classe Labrador est issue d’une bibliothèque tierce, nous ne pouvons pas non plus modifier le code source pour changer la visibilité...
La réification de type
1. La problématique
Commençons par un exemple qui met en avant le problème que la réification de type nous aidera à corriger.
Soit une fonction printClass dont le but est d’afficher l’interface Class d’une interface KClass que l’on passe en paramètre. L’interface KClass est générique, et pour respecter cette généricité, la fonction doit pouvoir être utilisée avec n’importe quelle classe.
fun printClass(type: Kclass<T>)
{
//...
}
Malheureusement, en l’état, cette fonction ne compile pas puisque le type générique T est inconnu. Modifions-la pour introduire le type générique :
fun <T> printClass(type: Kclass<T>)
{
//...
}
Nous avons toujours une erreur à la compilation. En effet, l’interface KClass impose de préciser que le type générique doit être de type Any :
fun <T : Any> printClass(type: Kclass<T>)
{
//...
}
En l’état, nous n’avons plus de problème de compilation.
Complétons donc la fonction pour afficher l’interface Class et utiliser la fonction dans un programme complet :
import kotlin.reflect.KClass
class Dog
fun main() ...
En résumé
-
L’introspection est également appelée réflexivité.
-
L’introspection permet de parcourir dynamiquement les éléments d’une classe (méthodes, attributs, constructeurs, etc).
-
En Kotlin, l’introspection se fait à partir de la classe KClass tandis qu’en Java elle se fait à partir de la classe Class.
-
L’introspection permet d’instancier dynamiquement une classe.
-
L’introspection permet, notamment, de manipuler des éléments privés d’une classe (comme un attribut ou une fonction).
-
Historiquement, pour gérer les types génériques au niveau de la machine virtuelle Java (JVM), c’est le principe de l’effacement de type qui est utilisé.
-
Le principe de l’effacement de type est responsable de l’impossibilité de lire les informations d’un type générique lors de l’exécution d’un programme.
-
Pour contourner cette problématique, le langage Kotlin introduit la réification de type.
-
Pour mettre en œuvre ce concept, il convient d’utiliser le mot-clé reified.
-
La réification de type doit obligatoirement s’accompagner de l’utilisation d’une fonction "en ligne".