287 votes

Comment fonctionne le mot-clé réifié en Kotlin ?

J'essaye de comprendre le but de la reified mot-clé, apparemment il nous permet de faire de la réflexion sur les génériques .

Cependant, lorsque je ne l'utilise pas, il fonctionne tout aussi bien. Quelqu'un peut-il m'expliquer à quel moment cela devient un réel problème ? différence ?

11 votes

Les paramètres de type générique sont effacés au moment de l'exécution, lisez ce qui concerne l'effacement des types si ce n'est pas déjà fait. Les paramètres de type réifié sur les fonctions inline n'intègrent pas seulement le corps de la méthode, mais aussi les paramètres de type générique. paramètre de type générique vous permettant de faire des choses comme T::class.java (ce que vous ne pouvez pas faire avec les types génériques normaux). Je le mets en commentaire parce que je n'ai pas le temps d'étoffer une réponse complète pour le moment

1 votes

Il permet d'accéder au type générique concret d'une fonction sans s'appuyer sur la réflexion et sans avoir à passer le type en argument.

658voto

s1m0nw1 Points 21698

TL;DR : Qu'est-ce que reified pour

fun <T> myGenericFun(c: Class<T>) 

Dans le corps d'une fonction générique comme myGenericFun vous ne pouvez pas accéder au type T parce que c'est disponible uniquement au moment de la compilation mais effacé au moment de l'exécution. Par conséquent, si vous souhaitez utiliser le type générique comme une classe normale dans le corps de la fonction, vous devez passer explicitement la classe en tant que paramètre comme indiqué dans myGenericFun .

Si vous créez un inline avec une fonction réifié T le type de T peuvent être accédés même au moment de l'exécution, et donc vous n'avez pas besoin de passer l'attribut Class<T> en plus. Vous pouvez travailler avec T comme s'il s'agissait d'une classe normale - par exemple, vous pouvez vouloir vérifier si une variable est une classe de type exemple de T ce que vous pouvez facilement faire : myVar is T .

Un tel inline fonction avec reified type T se présente comme suit :

inline fun <reified T> myGenericFun()

Comment reified travaux

Vous pouvez uniquement utiliser reified en combinaison avec un inline fonction. Ce faisant, vous demandez au compilateur de copier le bytecode de la fonction à chaque endroit où la fonction est invoquée (le compilateur "inline" la fonction). Lorsque vous appelez une fonction inline fonction avec reified le compilateur doit être en mesure de connaître le type réel passé en argument de type afin de pouvoir modifier le bytecode généré pour utiliser directement la classe correspondante. Par conséquent, un appel comme myVar is T devient myVar is String dans le bytecode (si l'argument type est String ).


Exemple

Examinons un exemple qui montre l'utilité de cette méthode. reified peut être. Nous voulons créer une fonction d'extension pour String appelé toKotlinObject qui tente de convertir une chaîne JSON en un objet Kotlin ordinaire dont le type est spécifié par le type générique de la fonction. T . Nous pouvons utiliser com.fasterxml.jackson.module.kotlin pour cela et la première approche est la suivante :

a) Première approche sans type réifié

fun <T> String.toKotlinObject(): T {
      val mapper = jacksonObjectMapper()
                                                        //does not compile!
      return mapper.readValue(this, T::class.java)
}

En readValue prend un type qu'elle est censée analyser le fichier JsonObject à. Si nous essayons d'obtenir le Class du paramètre de type T le compilateur se plaint : "Impossible d'utiliser 'T' comme paramètre de type réifié. Utilisez une classe à la place".

b) Contournement avec explicite Class paramètre

fun <T: Any> String.toKotlinObject(c: KClass<T>): T {
    val mapper = jacksonObjectMapper()
    return mapper.readValue(this, c.java)
}

Comme solution de contournement, le Class de T peut être transformé en paramètre de méthode, qui sera ensuite utilisé comme argument pour readValue . Cela fonctionne et constitue un modèle courant dans le code Java générique. Il peut être appelé comme suit :

data class MyJsonType(val name: String)

val json = """{"name":"example"}"""
json.toKotlinObject(MyJsonType::class)

c) La méthode Kotlin : reified

En utilisant un inline fonction avec reified paramètre de type T permet d'implémenter la fonction différemment :

inline fun <reified T: Any> String.toKotlinObject(): T {
    val mapper = jacksonObjectMapper()
    return mapper.readValue(this, T::class.java)
}

Il n'y a pas besoin de prendre le Class de T en plus, T peut être utilisé comme s'il s'agissait d'une classe ordinaire. Pour le client, le code ressemble à ceci :

json.toKotlinObject<MyJsonType>()

Note importante : travailler avec Java

Une fonction inlined avec reified le type est non appelable depuis Java code.

15 votes

Merci pour votre réponse complète ! C'est tout à fait logique. Juste une chose que je me demande, pourquoi la réification est nécessaire si la fonction est inline de toute façon ? On laisse l'effacement du type et on inline la fonction de toute façon ? Cela me semble être une sorte de gaspillage, si vous inlinez la fonction, vous pourriez aussi bien inlinez le type utilisé ou est-ce que je vois quelque chose de mal ici ?

16 votes

Merci pour votre commentaire. En fait, j'ai oublié de mentionner quelque chose qui pourrait vous donner la réponse : une fonction inline normale peut être appelée à partir de Java mais un paramètre de type réifié ne le peut pas ! Je pense que c'est une raison pour laquelle tous les paramètres de type d'une fonction inline ne sont pas automatiquement réifiés.

1 votes

Que faire si la fonction est un mélange de paramètres réifiés et non réifiés ? Cela la rend non éligible pour être appelée depuis Java de toute façon, pourquoi ne pas réifier tous les paramètres de type automatiquement ? Pourquoi Kotlin doit-il spécifier explicitement la réification pour tous les paramètres de type ?

55voto

Comprendre reified types

Génériques

En utilisant les génériques en Kotlin, nous pouvons effectuer des opérations sur une valeur de n'importe quel type T :

fun <T> doSomething(value: T) {
    println("Doing something with value: $value")                 // OK
}

Ici, nous appelons implicitement le toString() fonction de la value et ça marche.

Mais nous ne pouvons pas effectuer d'opérations sur le type T directement :

fun <T> doSomething(value: T) {
    println("Doing something with type: ${T::class.simpleName}")  // Error
}

Essayons de comprendre la raison de cette erreur.

Effacement de type

Dans le code ci-dessus, le compilateur donne une erreur : Cannot use 'T' as reified type parameter. Use a class instead. Cela se produit parce qu'au moment de la compilation, le compilateur supprime l'argument de type de l'appel de fonction.

Par exemple, si vous appelez la fonction comme :

doSomething<String>("Some String")

Le compilateur supprime la partie argument de type <String> et tout ce qui reste au moment de l'exécution est :

doSomething("Some String")

C'est ce qu'on appelle effacement de type . Ainsi, au moment de l'exécution (à l'intérieur de la définition de la fonction), nous ne pouvons pas savoir exactement quel est le type de la fonction T représente.

Solution Java

La solution à ce problème d'effacement de type en Java consistait à passer un argument supplémentaire spécifiant le type avec l'attribut Class (en Java) ou KClass (en Kotlin) :

fun <T: Any> doSomething(value: T, type: KClass<T>) {
    println("Doing something with type: ${type.simpleName}")       // OK
}

De cette façon, notre code n'est pas affecté par l'effacement des types. Mais cette solution est verbeuse et pas très élégante puisque nous devons la déclarer ainsi que l'appeler avec un argument supplémentaire. De plus, le fait de spécifier le type lié Any est obligatoire.

Réification du type

La meilleure solution au problème ci-dessus est la réification des types en Kotlin. Le site reified avant le paramètre de type permet de conserver les informations de type lors de l'exécution :

inline fun <reified T> doSomething(value: T) {
    println("Doing something with type: ${T::class.simpleName}")    // OK
}

Dans le code ci-dessus, grâce au reified nous n'obtenons plus l'erreur lors de l'exécution d'une opération sur le type T . Voyons comment inline rendent cette magie possible.

inline fonctions

Lorsque nous marquons une fonction comme inline le compilateur copie le corps réel de ce code. inline partout où cette fonction est appelée. Puisque nous avons marqué notre doSomething() fonction en tant que inline le code suivant :

fun main() {
    doSomething<String>("Some String")
}

est compilé :

fun main() {
    println("Doing something with type: ${String::class.simpleName}")
}

Ainsi, les deux extraits de code présentés ci-dessus sont équivalents.

En copiant le corps d'un inline le compilateur remplace également le paramètre de type T avec l'argument de type réel qui est spécifié ou déduit dans l'appel de fonction. Par exemple, remarquez comment le paramètre de type T est remplacé par l'argument de type réel String .


Contrôle de type et moulage de type de reified types

L'objectif principal d'un reified paramètre de type est de connaître le type exact que le paramètre de type T représente au moment de l'exécution.

Disons que nous avons une liste de différents types de fruits :

val fruits = listOf(Apple(), Orange(), Banana(), Orange())

Et nous voulons filtrer tous les Orange dans une liste séparée comme suit :

val oranges = listOf(Orange(), Orange())

Sans reified

Pour filtrer les types de fruits, nous pouvons écrire une fonction d'extension sur List<Any> comme les suivantes :

fun <T> List<Any>.filterFruit(): List<T> {
    return this.filter { it is T }.map { it as T }          // Error and Warning
}

Dans ce code, nous filtrons d'abord les types et ne prenons l'élément que si son type correspond à l'argument type donné. Ensuite, nous transférons chaque élément vers l'argument de type donné et return le site List . Mais il y a deux problèmes.

Vérification du type

Pendant la vérification du type it is T nous sommes confrontés à une autre erreur du compilateur : Cannot check for instance of erased type: T . Il s'agit d'un autre type d'erreur que vous pouvez rencontrer en raison de l'effacement des caractères.

Coulée de type

Alors que le moulage de type it as T on nous donne aussi un avertissement : Unchecked cast: Any to T . Le compilateur ne peut pas confirmer le type en raison de l'effacement du type.

reified Les types à la rescousse

Nous pouvons facilement surmonter ces deux problèmes en marquant la fonction comme étant inline et en rendant le paramètre de type reified comme expliqué précédemment :

inline fun <reified T> List<Any>.filterFruit(): List<T> {
    return this.filter { it is T }.map { it as T }
}

Et l'appeler comme suit :

val oranges = fruits.filterFruit<Orange>()

J'ai montré cette fonction pour faciliter la démonstration. Pour filtrer les types dans les collections, il existe déjà une fonction de la bibliothèque standard filterIsInstance() . Cette fonction a utilisé le inline y reified de la même manière. Vous pouvez simplement l'appeler comme suit :

val oranges = fruits.filterIsInstance<Orange>()

Passing reified en tant qu'argument

En reified permet à une fonction de transmettre le paramètre de type en tant qu'argument de type à une autre fonction qui possède le même type de paramètre. reified modificateur :

inline fun <reified T> doSomething() {
    // Passing T as an argument to another function
    doSomethingElse<T>()
}

inline fun <reified T> doSomethingElse() { }

Obtenir le type générique de la reified type

Parfois, un argument de type peut être un type générique. Par exemple, List<String> dans l'appel de fonction doSomething<List<String>>() . Il est possible de connaître ce type entier, grâce à la réification :

inline fun <reified T> getGenericType() {
    val type: KType = typeOf<T>()
    println(type)
}

Ici, le typeOf() est une fonction de la bibliothèque standard. Le site println() La fonction ci-dessus imprimera kotlin.collections.List<kotlin.String> si vous appelez la fonction comme getGenericType<List<String>>() . Le site KType comprend KClass les informations sur les arguments de type et les informations sur la nullité. Une fois que vous connaissez le KType vous pouvez y réfléchir.


Interopérabilité de Java

En inline Les fonctions déclarées sans reified peuvent être appelés depuis Java comme des fonctions Java ordinaires. Mais ceux qui sont déclarés avec l'attribut reified ne sont pas appelables depuis Java.

Même si vous l'appelez en utilisant la réflexion comme suit :

Method method = YourFilenameKt.class.getDeclaredMethod("doSomething", Object.class);
method.invoke("hello", Object.class);

Vous obtenez le UnsupportedOperationException: This function has a reified type parameter and thus can only be inlined at compilation time, not called directly.


Conclusion

Dans de nombreux cas, le reified nous aident à nous débarrasser des erreurs et avertissements suivants :

  1. Error: Cannot use 'T' as reified type parameter. Use a class instead.
  2. Error: Cannot check for instance of erased type: T
  3. Warning: Unchecked cast: SomeType to T

C'est ça ! J'espère que cela vous aidera à comprendre l'essence de reified types.

12voto

Aury0n Points 109

Le but de reified est de permettre à la fonction d'utiliser T au moment de la compilation (pour y accéder dans la fonction).

Par exemple :

inline fun <reified T:Any>  String.convertToObject(): T{
    val gson = Gson()
    return gson.fromJson(this,T::class.java)
}

A utiliser :

val jsonStringResponse = "{"name":"bruno" , "age":"14" , "world":"mars"}"
val userObject = jsonStringResponse.convertToObject<User>()
println(userObject.name)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X