Je sais que Java implémente le polymorphisme paramétrique (Generics) avec effacement. Je comprends ce qu’est l’effacement.
Je sais que C # implémente un polymorphisme paramétrique avec réification. Je sais que peut vous faire écrire
public void dosomething(List<String> input) {}
public void dosomething(List<Int> input) {}
ou que vous pouvez savoir au moment de l'exécution quel est le paramètre type d'un type paramétré, mais je ne comprends pas ce qu'il est.
La réification est le processus consistant à prendre une chose abstraite et à créer une chose concrète.
Le terme réification dans les génériques C # désigne le processus par lequel une définition de type générique et un ou plusieurs arguments de type génériques (la chose abstraite) sont combinés pour créer un nouvel type générique (la chose concrète).
Pour le formuler différemment, il s’agit de prendre la définition de List<T>
et int
et produire un béton List<int>
type.
Pour mieux comprendre, comparez les approches suivantes:
Dans les génériques Java, une définition de type générique est transformée en un type générique concret partagé par toutes les combinaisons d'arguments de type autorisées. Ainsi, plusieurs types (au niveau du code source) sont mappés à un (au niveau binaire) type - mais en conséquence, les informations sur les arguments de type d'une instance sont ignorées dans cette instance (type erasure) .
Dans les génériques C #, la définition de type générique est conservée en mémoire au moment de l'exécution. Lorsqu'un nouveau type concret est requis, l'environnement d'exécution combine la définition de type générique et les arguments de type et crée le nouveau type (réification). Nous obtenons donc un nouveau type pour chaque combinaison d'arguments de type, à l'exécution .
System.Type
class (même si la combinaison d’arguments de type générique que vous instanciez n’apparaissait pas directement dans votre code source).Dans les modèles C++, la définition du modèle est conservée en mémoire au moment de la compilation. Chaque fois qu'une nouvelle instanciation d'un type de modèle est requise dans le code source, le compilateur combine la définition du modèle et les arguments du modèle et crée le nouveau type. Nous obtenons donc un type unique pour chaque combinaison des arguments du template, lors de la compilation .
Réification signifie généralement (en dehors de l'informatique) "faire quelque chose de réel".
En programmation, quelque chose est réifié si nous pouvons accéder aux informations à ce sujet dans le langage lui-même.
Prenons deux méthodes et accès mémoire pour deux exemples totalement non liés aux médicaments génériques.
Les langages OO ont généralement méthodes, (et beaucoup qui n'ont pas fonctions qui sont similaires mais non liés à une classe). En tant que tel, vous pouvez définir une méthode dans un tel langage, l'appeler, éventuellement la remplacer, etc. Toutes ces langues ne vous permettent pas de traiter réellement la méthode elle-même en tant que données d'un programme. C # (et, en réalité, .NET plutôt que C #) vous permet d'utiliser les objets MethodInfo
représentant les méthodes, ainsi les méthodes en C # sont réifiées. Les méthodes en C # sont des "objets de première classe".
Toutes les langues pratiques ont des moyens d'accéder à la mémoire d'un ordinateur. Dans un langage de bas niveau comme C, nous pouvons nous occuper directement de la correspondance entre les adresses numériques utilisées par l'ordinateur. Par conséquent, le type de fonction int* ptr = (int*) 0xA000000; *ptr = 42;
est raisonnable (tant que nous avons de bonnes raisons de penser que l'accès adresse mémoire 0xA000000
de cette façon ne fera pas exploser quelque chose). En C #, cela n’est pas raisonnable (nous pouvons presque le forcer dans .NET, mais avec la gestion de la mémoire .NET, il n’est pas très utile que cela bouge). C # n'a pas d'adresses mémoire réifiées.
Donc, comme réfié signifie "rendu réel", un "type réifié" est un type que nous pouvons "parler" dans la langue en question.
En génériques, cela signifie deux choses.
L'un est que List<string>
est un type exactement comme string
ou int
. Nous pouvons comparer ce type, obtenir son nom et demander à ce sujet:
Console.WriteLine(typeof(List<string>).FullName); // System.Collections.Generic.List`1[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
Console.WriteLine(typeof(List<string>) == (42).GetType()); // False
Console.WriteLine(typeof(List<string>) == Enumerable.Range(0, 1).Select(i => i.ToString()).ToList().GetType()); // True
Console.WriteLine(typeof(List<string>).GenericTypeArguments[0] == typeof(string)); // True
Une conséquence de ceci est que nous pouvons "parler" des types de paramètres d'une méthode générique (ou d'une méthode générique) dans la méthode elle-même:
public static void DescribeType<T>(T element)
{
Console.WriteLine(typeof(T).FullName);
}
public static void Main()
{
DescribeType(42); // System.Int32
DescribeType(42L); // System.Int64
DescribeType(DateTime.UtcNow); // System.DateTime
}
En règle générale, faire trop, c'est "malodorant", mais cela présente de nombreux cas utiles. Par exemple, regardez:
public static TSource Min<TSource>(this IEnumerable<TSource> source)
{
if (source == null) throw Error.ArgumentNull("source");
Comparer<TSource> comparer = Comparer<TSource>.Default;
TSource value = default(TSource);
if (value == null)
{
using (IEnumerator<TSource> e = source.GetEnumerator())
{
do
{
if (!e.MoveNext()) return value;
value = e.Current;
} while (value == null);
while (e.MoveNext())
{
TSource x = e.Current;
if (x != null && comparer.Compare(x, value) < 0) value = x;
}
}
}
else
{
using (IEnumerator<TSource> e = source.GetEnumerator())
{
if (!e.MoveNext()) throw Error.NoElements();
value = e.Current;
while (e.MoveNext())
{
TSource x = e.Current;
if (comparer.Compare(x, value) < 0) value = x;
}
}
}
return value;
}
Cela ne fait pas beaucoup de comparaisons entre le type de TSource
et divers types pour différents comportements (généralement un signe que vous n’auriez pas dû utiliser de génériques), mais une division entre un chemin de code pour les types peut être null
(devrait retourner null
si aucun élément n'a été trouvé, et ne doit pas faire de comparaison pour trouver le minimum si l'un des éléments comparés est null
) et le chemin du code pour types qui ne peuvent pas être null
(doivent être jetés si aucun élément n'a été trouvé et ne doivent pas s'inquiéter de la possibilité d'éléments null
).
Étant donné que TSource
est "réel" dans la méthode, cette comparaison peut être effectuée au moment de l'exécution ou du lancement (généralement au moment de l'exécution, le cas ci-dessus le ferait certainement au moment de l'exécution et ne produirait pas de code machine pour le chemin d'accès). prises) et nous avons une version "réelle" distincte de la méthode pour chaque cas. (Bien qu'en tant qu'optimisation, le code machine est partagé pour différentes méthodes pour différents paramètres de type référence, parce qu'il peut l'être sans affecter cela et que nous pouvons donc réduire la quantité de code machine jitted).
(Il n’est pas courant de parler de réification de types génériques en C # à moins de traiter également avec Java, car en C #, nous prenons cette réification pour acquise; tous les types sont réifiés. En Java, les types non génériques sont appelés réifié car c'est une distinction entre eux et les types génériques).
Comme duffymo déjà noté , "la réification" n'est pas la différence principale.
En Java, les génériques sont essentiellement là pour améliorer la prise en charge au moment de la compilation - cela vous permet d’utiliser des caractères fortement typés, par exemple. collections dans votre code, et que le type sécurité soit traité pour vous. Cependant, cela n’existe qu’au moment de la compilation: le bytecode compilé n’a plus aucune notion de générique; tous les types génériques sont transformés en types "concrets" (en utilisant object
si le type générique n'est pas lié), en ajoutant des conversions de types et des vérifications de types si nécessaire.
Dans .NET, les génériques font partie intégrante du CLR. Lorsque vous compilez un type générique, il reste générique dans l'IL généré. Ce n'est pas simplement transformé en code non générique comme en Java.
Cela a plusieurs impacts sur le fonctionnement pratique des génériques. Par exemple:
SomeType<?>
Pour vous permettre de passer n'importe quelle implémentation concrète d'un type générique donné. C # ne peut pas faire cela - chaque type générique spécifique ( réifié ) est son propre type.object
. Cela peut avoir un impact sur les performances lorsque vous utilisez des types de valeur dans ces génériques. En C #, lorsque vous utilisez une type de valeur dans un type générique, il reste un type de valeur.Pour donner un exemple, supposons que vous ayez un type générique List
avec un argument générique. En Java, List<String>
Et List<Int>
Finiront par être exactement du même type au moment de l'exécution - les types génériques n'existent réellement que pour le code à la compilation. Tous les appels vers, par exemple, GetValue
sera transformé en (String)GetValue
et (Int)GetValue
respectivement.
En C #, List<string>
Et List<int>
Sont deux types différents. Ils ne sont pas interchangeables et leur sécurité de type est également appliquée lors de l'exécution. Quoi que vous fassiez, new List<int>().Add("SomeString")
ne fonctionnera jamais - le stockage sous-jacent dans List<int>
Est vraiment un tableau entier, alors qu’en Java, il s’agit nécessairement d’un tableau object
. En C #, il n'y a pas de casting, pas de boxe, etc.
Cela devrait également expliquer pourquoi C # ne peut pas faire la même chose que Java avec SomeType<?>
. En Java, tous les types génériques "dérivés de" SomeType<?>
En fin de compte, le même type est exact. En C #, tous les différents types SomeType<T>
correspondent à un type distinct. En supprimant les contrôles au moment de la compilation, il est possible de passer SomeType<Int>
au lieu de SomeType<String>
(et, en réalité, tout ce que SomeType<?>
signifie, c’est "ignorer les contrôles de compilation pour le type générique donné"). En C #, ce n’est pas possible, pas même pour les types dérivés (vous ne pouvez pas do List<object> list = (List<object>)new List<string>();
même si string
est dérivé de object
).
Les deux implémentations ont leurs avantages et leurs inconvénients. Il y a eu quelques fois où j'aurais aimé pouvoir simplement autoriser SomeType<?>
Comme argument en C # - mais cela n'a tout simplement aucun sens pour le fonctionnement des génériques C #.
La réification est un concept de modélisation orienté objet.
Reify est un verbe qui signifie "rend réel quelque chose d'abstrait" .
Lorsque vous effectuez une programmation orientée objet, il est courant de modéliser des objets du monde réel sous forme de composants logiciels (par exemple, une fenêtre, un bouton, une personne, une banque, un véhicule, etc.).
Il est également courant de réifier des concepts abstraits en composants (exemple: WindowListener, Broker, etc.).