Génériques à Kotlin

Génériques à Kotlin

1. Vue d'ensemble

Dans cet article, nous allons examiner lesgeneric types in the Kotlin language.

Ils sont très similaires à ceux du langage Java, mais les créateurs du langage Kotlin ont essayé de les rendre un peu plus intuitifs et compréhensibles en introduisant des mots clés spéciaux commeout etin.

3. Mots clés Kotlinout etin

3.1. Le mot-cléOut

Disons que nous voulons créer une classe de producteurs qui produira un résultat de type T. Parfois; nous voulons affecter cette valeur produite à une référence qui est un sur-type du type T.

Pour y parvenir en utilisant Kotlin,we need to use theout keyword on the generic type. It means that we can assign this reference to any of its supertypes. The out value can be only be produced by the given class but not consumed:

class ParameterizedProducer(private val value: T) {
    fun get(): T {
        return value
    }
}

Nous avons défini une classeParameterizedProducer qui peut produire une valeur de type T.

Suivant; nous pouvons attribuer une instance de la classeParameterizedProducer à la référence qui en est un supertype:

val parameterizedProducer = ParameterizedProducer("string")

val ref: ParameterizedProducer = parameterizedProducer

assertTrue(ref is ParameterizedProducer)

Si le typeT dans la classeParamaterizedProducer ne sera pas du typeout, l'instruction donnée produira une erreur du compilateur.

3.2. Le mot-cléin

Parfois, nous avons une situation opposée signifiant que nous avons une référence de typeT et nous voulons pouvoir l'assigner au sous-type deT.

We can use the in keyword on the generic type if we want to assign it to the reference of its subtype. The in keyword can be used only on the parameter type that is consumed, not produced:

class ParameterizedConsumer {
    fun toString(value: T): String {
        return value.toString()
    }
}

Nous déclarons qu'une méthodetoString() ne consommera qu'une valeur de typeT.

Ensuite, nous pouvons attribuer une référence de typeNumber à la référence de son sous-type -Double:

val parameterizedConsumer = ParameterizedConsumer()

val ref: ParameterizedConsumer = parameterizedConsumer

assertTrue(ref is ParameterizedConsumer)

Si le typeT dans leParameterizedCounsumer ne sera pas le typein, l'instruction donnée produira une erreur de compilation.

4. Projections de type

4.1. Copier un tableau de sous-types dans un tableau de supertypes

Disons que nous avons un tableau d'un certain type, et que nous voulons copier le tableau entier dans le tableau de typeAny. C'est une opération valide, mais pour permettre au compilateur de compiler notre code, nous devons annoter le paramètre d'entrée avec le mot cléout.

Cela permet au compilateur de savoir que l'argument d'entrée peut être de n'importe quel type qui est un sous-type desAny:

fun copy(from: Array, to: Array) {
    assert(from.size == to.size)
    for (i in from.indices)
        to[i] = from[i]
}

Si le paramètrefrom n'est pas du typeout Any, nous ne pourrons pas passer un tableau de typeInt comme argument:

val ints: Array = arrayOf(1, 2, 3)
val any: Array = arrayOfNulls(3)

copy(ints, any)

assertEquals(any[0], 1)
assertEquals(any[1], 2)
assertEquals(any[2], 3)

4.2. Ajout d'éléments d'un sous-type à un tableau de son supertype

Disons que nous avons la situation suivante - nous avons un tableau de typeAny qui est un supertype deInt et nous voulons ajouter un élémentInt à ce tableau. Nous devons utiliser le mot-cléin comme type du tableau de destination pour indiquer au compilateur que nous pouvons copier la valeur deInt dans ce tableau:

fun fill(dest: Array, value: Int) {
    dest[0] = value
}

Ensuite, nous pouvons copier une valeur de typeInt dans le tableau deAny:

val objects: Array = arrayOfNulls(1)

fill(objects, 1)

assertEquals(objects[0], 1)

4.3. Projections d'étoiles

Il y a des situations où nous ne nous soucions pas du type spécifique de la valeur. Disons que nous voulons simplement imprimer tous les éléments d'un tableau et peu importe le type des éléments de ce tableau.

Pour y parvenir, nous pouvons utiliser une projection en étoile:

fun printArray(array: Array<*>) {
    array.forEach { println(it) }
}

Ensuite, nous pouvons passer un tableau de n'importe quel type à la méthodeprintArray():

val array = arrayOf(1,2,3)
printArray(array)

Lorsque vous utilisez le type de référence de projection en étoile, vous pouvez en lire les valeurs, mais nous ne pouvons pas les écrire car cela provoquerait une erreur de compilation.

5. Contraintes génériques

Disons que nous voulons trier un tableau d’éléments et que chaque type d’élément doit implémenter une interfaceComparable. Nous pouvons utiliser les contraintes génériques pour spécifier cette exigence:

fun > sort(list: List): List {
    return list.sorted()
}

Dans l'exemple donné, nous avons défini que tous les élémentsT nécessaires pour implémenter l'interfaceComparable. Sinon, si nous essayons de passer une liste d'éléments qui n'implémentent pas cette interface, cela entraînera une erreur du compilateur.

Nous avons défini une fonctionsort qui prend comme argument une liste d'éléments qui implémententComparable, afin que nous puissions appeler la méthodesorted() dessus. Examinons le cas de test pour cette méthode:

val listOfInts = listOf(5,2,3,4,1)

val sorted = sort(listOfInts)

assertEquals(sorted, listOf(1,2,3,4,5))

On peut facilement passer une liste deInts car le typeInt implémente l'interfaceComparable.

6. Génériques à l'exécution

6.1. Type d'effacement

Comme pour Java, les génériques de Kotlin sonterased à l'exécution. Autrement dit,an instance of a generic class doesn’t preserve its type parameters at runtime.

Par exemple, si nous créons unSet<String> et y mettons quelques chaînes, au moment de l'exécution, nous ne pouvons le voir que comme unSet.

Créons deuxSets avec deux paramètres de type différents:

val books: Set = setOf("1984", "Brave new world")
val primes: Set = setOf(2, 3, 11)

Au moment de l'exécution, les informations de type pourSet<String> etSet<Int> seront effacées et nous les voyons tous les deux comme desSets.  simples, même s'il est parfaitement possible de découvrir au moment de l'exécution que la valeur est unSet, nous ne pouvons pas dire s'il s'agit d'unSet de chaînes, d'entiers ou d'autre chose:that information has been erased.

Alors, comment le compilateur de Kotlin nous empêche-t-il d'ajouter unNon-String dans unSet<String>? Ou, quand nous obtenons un élément d'unSet<String>, comment sait-il que l'élément est unString?

La réponse est simple The compiler is the one responsible for erasing the type information mais avant cela, il sait en fait que la variablebooks contient les élémentsString.

Ainsi, chaque fois que nous en obtenons un élément, le compilateur le convertit enString ou lorsque nous y ajoutons un élément, le compilateur tape check l'entrée.

6.2. Paramètres de type réifiés

Amusons-nous davantage avec les génériques et créons une fonction d'extension pour filtrer les élémentsCollection en fonction de leur type:

fun  Iterable<*>.filterIsInstance() = filter { it is T }
Error: Cannot check for instance of erased type: T

La partie «it is T”, pour chaque élément de collection, vérifie si l'élément est une instance de typeT, mais comme les informations de type ont été effacées à l'exécution, nous ne pouvons pas réfléchir sur les paramètres de type de cette façon .

Ou pouvons-nous?

La règle d'effacement de type est vraie en général, mais il y a un cas où nous pouvons éviter cette limitation:Inline functions. Type parameters of inline functions can be reified, so we can refer to those type parameters at runtime.

Le corps des fonctions en ligne est en ligne. C'est-à-dire que le compilateur substitue le corps directement aux endroits où la fonction est appelée à la place de l'appel de la fonction normale.

Si nous déclarons la fonction précédente commeinline et marquons le paramètre de type commereified, alors nous pouvons accéder aux informations de type générique au moment de l'exécution:

inline fun  Iterable<*>.filterIsInstance() = filter { it is T }

La réification en ligne fonctionne comme un charme:

>> val set = setOf("1984", 2, 3, "Brave new world", 11)
>> println(set.filterIsInstance())
[2, 3, 11]

Écrivons un autre exemple. Nous connaissons tous ces définitions typiques de SLF4jLogger:

class User {
    private val log = LoggerFactory.getLogger(User::class.java)

    // ...
}

En utilisant des fonctions en ligne réifiées, nous pouvons écrire des définitionsLogger plus élégantes et moins horribles pour la syntaxe:

inline fun  logger(): Logger = LoggerFactory.getLogger(T::class.java)

Ensuite, nous pouvons écrire:

class User {
    private val log = logger()

    // ...
}

6.3. Plongez au cœur de la réification en ligne

Alors, quelle est la particularité des fonctions en ligne pour que la réification de type fonctionne uniquement avec elles? Comme nous le savons, le compilateur de Kotlin copie le bytecode des fonctions en ligne dans les endroits où la fonction est appelée.

Étant donné que dans chaque site d'appel, le compilateur connaît le type de paramètre exact, il peut remplacer le paramètre de type générique par les références de type réelles.

Par exemple, quand on écrit:

class User {
    private val log = logger()

    // ...
}

When the compiler inlines the logger<User>() function call, it knows the actual generic type parameter –User. Ainsi, au lieu d'effacer les informations de type, le compilateur saisit l'opportunité de réification et réifie le paramètre de type réel.

7. Conclusion

Dans cet article, nous examinions les types génériques de Kotlin. Nous avons vu comment utiliser correctement les mots-clésout etin. Nous avons utilisé des projections de type et défini une méthode générique utilisant des contraintes génériques.

L'implémentation de tous ces exemples et extraits de code peut être trouvée dans leGitHub project - il s'agit d'un projet Maven, il devrait donc être facile à importer et à exécuter tel quel.