Publié par
Il y a 3 mois · 6 minutes · Back, Craft

Les KProperty2 ou la réflexion signée Kotlin

En parcourant la bibliothèque standard de Kotlin, section réflexion, on peut tomber sur des types tels que KProperty0, KProperty1 et KProperty2.

On comprend assez rapidement que les types KProperty* sont des types qui représentent et permettent de manipuler, par réflexion, des propriétés i.e. des variables déclarées dans un package ou dans une classe. On comprend, en revanche, un peu plus difficilement pourquoi des propriétés peuvent être représentées par plusieurs types (et surtout par trois types différents). En Java, par exemple, l’équivalent d’une propriété – un champ – est toujours représenté par le type Field, qu’il soit statique ou non.

Cet article vous propose de découvrir ce que les différents types KProperty* représentent. Nous commencerons donc par étudier les types KProperty0 et KProperty1 qui se révèlent, au final, assez simples. Nous pourrons ensuite mieux aborder le type KProperty2, la configuration un peu inhabituelle qu’il représente et surtout en quoi il peut bien nous être utile.

Le type KProperty0

Comme le montre le code ci-dessous (lien gist), le type KProperty0 représente une variable immuable déclarée au niveau d’un package (sans aucun contexte).

package com.github.sergiords.kproperties

import kotlin.reflect.KProperty0

val name: String = "Bob"

fun main(args: Array<String>) {

    val kProperty0: KProperty0<String> = ::name

    val value: String = kProperty0() // value of the property "name"

    println(value) // prints "Bob"

}

Dans cet exemple, pour obtenir une instance du type KProperty0 représentant la variable immuable name on utilise l’opérateur « :: » (ligne 9) appliqué à la variable elle-même. On voit que le type KProperty0 est paramétré par le type de la propriété qu’il représente (String). L’instance kProperty0 peut ensuite être utilisée pour récupérer la valeur de la variable (ligne 11).

Le type KProperty1

Le code ci-dessous (lien gist) met quant à lui en évidence le type KProperty1 qui représente une variable immuable déclarée au niveau d’une classe (dans le contexte d’un type User).

package com.github.sergiords.kproperties

import kotlin.reflect.KProperty1

class User {

    val name: String = "Bob"

}

fun main(args: Array<String>) {

    val kProperty1: KProperty1<User, String> = User::name

    val user: User = User()

    val value: String = kProperty1(user) // value of the property "name" inside "user" instance

    println(value) // prints "Bob"

}

Pour obtenir une instance du type KProperty1 représentant la variable immuable name dans une instance du type User on utilise l’opérateur « :: » appliqué au type User (ligne 13). On voit que le type KProperty1 est paramétré par le type contenant la variable (User) et par le type de la propriété qu’il représente (String). L’instance kProperty1 peut ensuite être utilisée pour récupérer la valeur de la variable immuable dans une instance du type User (ligne 17).

Le type KProperty2

Nous venons de voir les types KProperty0 et KProperty1, ils représentent respectivement une variable immuable déclarée au niveau d’un package (zéro contexte) et au niveau d’un type donné (un seul contexte, l’instance). Pour le type KProperty2 on suit le même raisonnement en ajoutant un contexte. Le type KProperty2 représente donc une variable immuable déclarée dans deux contextes. Comment est-ce possible ? Avec les extensions de propriétés, mises en évidence ci-dessous (lien gist).

package com.github.sergiords.kproperties

import kotlin.reflect.KProperty2
import kotlin.reflect.full.declaredMemberExtensionProperties

class Magic {

    val String.magicLength: Int   // add property magicLength to String type inside Magic class only
        get() = this.length * 42

    fun length(arg: String): Int = arg.magicLength

}

fun main(args: Array<String>) {

    @Suppress("UNCHECKED_CAST")
    val kProperty2: KProperty2<Magic, String, Int> = Magic::class.declaredMemberExtensionProperties.single() as KProperty2<Magic, String, Int>

    val magic = Magic()

    val abcdValue: Int = kProperty2(magic, "ABCD") // value of the property "magicLength" inside "ABCD" and "magic" instances
    println(abcdValue) // since "ABCD".length = 4, prints 4 * 42 = 168

    val abcdeValue: Int = magic.length("ABCDE") // value of the property "magicLength" inside "ABCDE" and "magic" instances
    println(abcdeValue) // since "ABCDE".length = 5, prints 5 * 42 = 210

}

La syntaxe un peu particulière des lignes 8 et 9 permet de rattacher une variable immuable magicLength, au type String, uniquement accessible dans une instance du type Magic. Le this de la ligne 9 représente bien l’instance du type String qui reçoit cette nouvelle variable immuable. Cette syntaxe est une extension de propriété un peu particulière car elle est déclarée dans le contexte d’un autre type et c’est cette configuration de variable immuable que représente le type KProperty2.

Pour obtenir une instance du type KProperty2, on doit cette fois s’appuyer sur les extensions de propriétés déclarées au niveau du type Magic (ligne 18). On voit bien que le type KProperty2 est paramétré par le type dans lequel il est déclaré (Magic), par le type qui reçoit l’extension de propriété (String) et par le type de la propriété qu’il représente (Int). L’instance de kProperty2 peut ensuite être utilisée pour obtenir la valeur de la variable immuable magicLength d’une chaîne « ABCD » dans une instance donnée du type Magic (ligne 22).

Une utilisation plus standard de ces propriétés est la fonction Magic::length qui utilise cette extension de propriété, mais cette fois, directement dans le type Magic (déclaration à la ligne 11 et appel à la ligne 25).

Conclusion

L’objectif de cet article était avant tout de comprendre ce que les différents types KProperty* pouvaient bien représenter comme configuration. Si les types KProperty0 et KProperty1 représentent une configuration assez classique, il n’en est rien pour le type KProperty2.

La question qui nous vient alors naturellement à l’esprit est la suivante : « mais à quoi cela peut-il bien servir ? ». On peut voir le type KProperty2 comme la possibilité de représenter des propriétés qui ne sont valables que lorsque deux contextes sont réunis. On peut, par exemple, représenter la position d’une forme dans un conteneur avec le code ci-dessous (lien gist).

package com.github.sergiords.kproperties

class Form(val x: Int, val y: Int)

class Container(val x: Int, val y: Int) {

    val Form.xInContainer: Int
        get() = this.x - this@Container.x

    val Form.yInContainer: Int
        get() = this.y - this@Container.y

}

Dans l’exemple ci-dessus, une forme peut ainsi avoir une position absolue (Form.x, Form.y) et une position relative définie dans un conteneur (Form.xInContainer, Form.yInContainer). Cette dernière position bien que rattachée au type Form, n’est définie que dans le contexte du type Container. En dehors de ce contexte elle n’a pas de sens et n’est d’ailleurs pas accessible.

Sergio Dos Santos

Craft / DevOps / Back / Front / Cloud

One thought on “Les KProperty2 ou la réflexion signée Kotlin”

  1. Publié par Ampire, Il y a 3 mois

    c’était très clair merci

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *