Qu'est-ce que la réification?

163

Je sais que Java implémente le polymorphisme paramétrique (génériques) avec effacement. Je comprends ce qu'est l'effacement.

Je sais que C # implémente le polymorphisme paramétrique avec réification. Je sais que ça peut te faire écrire

public void dosomething(List<String> input) {}
public void dosomething(List<Int> input) {}

ou que vous pouvez savoir lors de l' exécution ce que le paramètre de type d' un certain type est paramétré, mais je ne comprends pas ce qu'il est .

  • Qu'est-ce qu'un type réifié?
  • Qu'est-ce qu'une valeur réifiée?
  • Que se passe-t-il lorsqu'un type / une valeur est réifié?
Martijn
la source
Ce n'est pas une réponse, mais cela peut aider d'une manière ou d'une autre: beust.com/weblog/2011/07/29/erasure-vs-reification
heringer
@heringer qui semble répondre assez bien à la question "qu'est-ce que l'effacement", et semble essentiellement répondre "qu'est-ce que la réification" par "ne pas effacer" - un thème commun que j'ai trouvé lors de la recherche initiale d'une réponse avant de poster ici.
Martijn
5
... et je pensais que la ré ification est le processus de conversion d'une switchconstruction en un if/ else, alors qu'elle avait déjà été convertie d'un if/ elseà un switch...
Digital Trauma
8
Res , reis est latin chose , donc réification est littéralement chosification . Je n'ai rien d'utile à apporter en ce qui concerne l'utilisation du terme en C #, mais le fait en soi qu'ils l'ont utilisé me fait sourire.
KRyan

Réponses:

209

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 # fait référence au processus par lequel une définition de type générique et un ou plusieurs arguments de type générique (la chose abstraite) sont combinés pour créer un nouveau type générique (la chose concrète).

Pour l' exprimer autrement, il est le processus de prendre la définition List<T>et intet la production d' un béton de 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 essentiellement transformée en un type générique concret partagé entre toutes les combinaisons d'arguments de type autorisées. Ainsi, plusieurs types (au niveau du code source) sont mappés à un type (niveau binaire) - mais par conséquent, les informations sur les arguments de type d'une instance sont ignorées dans cette instance (effacement de type) .

    1. En tant qu'effet secondaire de cette technique d'implémentation, les seuls arguments de type générique qui sont nativement autorisés sont les types qui peuvent partager le code binaire de leur type concret; ce qui signifie les types dont les emplacements de stockage ont des représentations interchangeables; ce qui signifie des types de référence. L'utilisation de types valeur comme arguments de type générique nécessite de les encadrer (en les plaçant dans un simple wrapper de type référence).
    2. Aucun code n'est dupliqué pour implémenter les génériques de cette manière.
    3. Les informations de type qui auraient pu être disponibles au moment de l'exécution (à l'aide de la réflexion) sont perdues. Ceci, à son tour, signifie que la spécialisation d'un type générique (la possibilité d'utiliser un code source spécialisé pour toute combinaison d'arguments génériques particulière) est très limitée.
    4. Ce mécanisme ne nécessite pas de prise en charge de l'environnement d'exécution.
    5. Il existe quelques solutions de contournement pour conserver les informations de type qu'un programme Java ou un langage basé sur JVM peut utiliser.
  • Dans les génériques C #, la définition de type générique est conservée en mémoire lors de l'exécution. Chaque fois qu'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, au moment de l'exécution .

    1. Cette technique d'implémentation permet à tout type de combinaison d'arguments de type d'être instancié. L'utilisation de types valeur comme arguments de type générique ne provoque pas de boxing, car ces types ont leur propre implémentation. (La boxe existe toujours en C # , bien sûr - mais cela se produit dans d'autres scénarios, pas celui-ci.)
    2. La duplication de code peut être un problème - mais en pratique ce n'est pas le cas, car des implémentations suffisamment intelligentes ( cela inclut Microsoft .NET et Mono ) peuvent partager du code pour certaines instanciations.
    3. Les informations de type sont conservées, ce qui permet une spécialisation dans une certaine mesure, en examinant les arguments de type à l'aide de la réflexion. Cependant, le degré de spécialisation est limité, en raison du fait qu'une définition de type générique est compilée avant toute réification (cela se fait en compilant la définition contre les contraintes sur les paramètres de type - ainsi, le compilateur doit être capable "comprendre" la définition même en l'absence d'arguments de type spécifiques ).
    4. Cette technique d'implémentation dépend fortement de la prise en charge de l'exécution et de la compilation JIT (c'est pourquoi vous entendez souvent que les génériques C # ont certaines limitations sur des plates-formes comme iOS , où la génération de code dynamique est restreinte).
    5. Dans le contexte des génériques C #, la réification est effectuée pour vous par l'environnement d'exécution. Cependant, si vous voulez comprendre plus intuitivement la différence entre une définition de type générique et un type générique concret, vous pouvez toujours effectuer une réification par vous-même, en utilisant la System.Typeclasse (même si la combinaison d'arguments de type générique particulier que vous instanciez n'a pas t apparaissent 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 de modèle et les arguments de modèle et crée le nouveau type. Nous obtenons donc un type unique pour chaque combinaison des arguments du modèle, au moment de la compilation .

    1. Cette technique d'implémentation permet à tout type de combinaison d'arguments de type d'être instancié.
    2. Ceci est connu pour dupliquer le code binaire, mais une chaîne d'outils suffisamment intelligente pourrait toujours le détecter et partager du code pour certaines instanciations.
    3. La définition du modèle elle-même n'est pas "compilée" - seules ses instanciations concrètes sont effectivement compilées . Cela place moins de contraintes sur le compilateur et permet un plus grand degré de spécialisation des modèles .
    4. Étant donné que les instanciations de modèle sont effectuées au moment de la compilation, aucun support d'exécution n'est nécessaire ici non plus.
    5. Ce processus est récemment appelé monomorphisation , en particulier dans la communauté Rust. Le mot est utilisé par opposition au polymorphisme paramétrique , qui est le nom du concept dont proviennent les génériques.
Theodoros Chatzigiannakis
la source
7
Excellente comparaison avec les modèles C ++ ... ils semblent se situer quelque part entre les génériques C # et Java. Vous avez un code et une structure différents pour gérer différents types génériques spécifiques comme en C #, mais tout est fait au moment de la compilation comme en Java.
Luaan
3
En outre, en C ++, cela permet d'introduire une spécialisation de modèle, où chaque (ou seulement certains) types concrets peuvent avoir des implémentations différentes. Evidemment pas possible en Java, mais ni en C #.
quetzalcoatl
@quetzalcoatl, bien qu'une des raisons d'utiliser cela soit de réduire la quantité de code produit avec des types de pointeurs, et C # fait quelque chose de comparable avec les types de référence en arrière-plan. Pourtant, c'est seulement une raison d'utiliser cela, et il y a certainement des moments où la spécialisation de modèle serait bien.
Jon Hanna
Pour Java, vous pouvez ajouter que lorsque les informations de type sont effacées, les transtypages sont ajoutés par le compilateur, rendant le bytecode indiscernable du bytecode pré-générique.
Rusty Core
27

La 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 à des informations à son sujet dans le langage lui-même.

Pour deux exemples complètement non génériques de quelque chose que C # fait et n'a pas réifié, prenons les méthodes et l'accès à la mémoire.

Les langages OO ont généralement des méthodes (et beaucoup d'entre eux n'ont pas de fonctions similaires mais non liées à une classe). En tant que tel, vous pouvez définir une méthode dans un tel langage, l'appeler, peut-être la remplacer, etc. Tous ces langages ne vous permettent pas de traiter la méthode elle-même en tant que données pour un programme. C # (et vraiment, .NET plutôt que C #) vous permet d'utiliser des MethodInfoobjets représentant les méthodes, donc en C #, les méthodes sont réifiées. Les méthodes en C # sont des "objets de première classe".

Tous les langages pratiques ont des moyens d'accéder à la mémoire d'un ordinateur. Dans un langage de bas niveau comme C, nous pouvons traiter directement le mappage entre les adresses numériques utilisées par l'ordinateur, donc les goûts int* ptr = (int*) 0xA000000; *ptr = 42;sont raisonnables (tant que nous avons une bonne raison de soupçonner que l'accès à l'adresse mémoire 0xA000000de cette manière a gagné '' t faire sauter quelque chose). En C #, ce n'est pas raisonnable (nous pouvons à peu près le forcer dans .NET, mais avec la gestion de la mémoire .NET, ce n'est pas très susceptible d'être utile). C # n'a pas d'adresses mémoire réifiées.

Ainsi, comme refied signifie «rendu réel», un «type réifié» est un type dont nous pouvons «parler» dans la langue en question.

En générique, cela signifie deux choses.

La première est que List<string>c'est un type tel stringou tel int. Nous pouvons comparer ce type, obtenir son nom et s'enquérir:

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 d'une classe 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, en faire trop 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 TSourceet divers types pour différents comportements (généralement un signe que vous n'auriez pas dû utiliser du tout des génériques) mais il se partage entre un chemin de code pour les types qui peuvent être null(devrait retourner nullsi aucun élément trouvé, et ne doit pas faire de comparaisons pour trouver le minimum si l'un des éléments comparés est null) et le chemin du code pour les types qui ne peuvent pas l'être null(devrait lancer si aucun élément trouvé, et n'a pas à s'inquiéter de la possibilité d' nulléléments ).

Parce qu'il TSourceest "réel" dans la méthode, cette comparaison peut être faite soit au moment de l'exécution, soit au moment du jitting (généralement au moment du jitting, certainement le cas ci-dessus le ferait au moment du jitting et ne produirait pas de code machine pour le chemin non pris) et nous avons un version «réelle» distincte de la méthode pour chaque cas. (Bien qu'à titre d'optimisation, le code machine est partagé pour différentes méthodes pour différents paramètres de type de référence, car cela peut être sans affecter cela, et nous pouvons donc réduire la quantité de code machine jit).

(Il n'est pas courant de parler de réification de types génériques en C #, sauf si vous traitez également avec Java, car en C # nous tenons simplement 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és car cela est une distinction entre eux et les types génériques).

Jon Hanna
la source
Vous ne pensez pas pouvoir faire ce Minque ci-dessus est utile? Sinon, il est très difficile de respecter son comportement documenté.
Jon Hanna
Je considère que le bogue est le comportement (non) documenté et l'implication que ce comportement est utile (en passant, le comportement de Enumerable.Min<TSource>est différent en ce qu'il ne lance pas pour les types non-référence sur une collection vide, mais renvoie par défaut (TSource), et est documenté uniquement comme "Renvoie la valeur minimale dans une séquence générique." Je dirais que les deux devraient jeter sur une collection vide, ou qu'un élément "zéro" doit être passé comme référence, et le comparateur / la fonction de comparaison doit toujours être transmise)
Martijn
1
Ce serait beaucoup moins utile que l'actuel Min, qui correspond au comportement de base de données courant sur les types Nullable sans tenter l'impossible sur les types non Nullable. (L'idée de base n'est pas impossible, mais pas très utile à moins qu'il n'y ait une valeur dont vous pouvez savoir qu'elle ne serait jamais dans la source).
Jon Hanna
1
Thingification aurait été un meilleur nom pour cela. :)
tchrist
@tchrist une chose peut être irréelle.
Jon Hanna
15

Comme Duffymo l'a déjà noté , la «réification» n'est pas la principale différence.

En Java, les génériques sont essentiellement là pour améliorer le support au moment de la compilation - cela vous permet d'utiliser des collections fortement typées par exemple dans votre code, et de gérer la sécurité des types pour vous. Cependant, cela n'existe qu'au moment de la compilation - le bytecode compilé n'a plus aucune notion de génériques; tous les types génériques sont transformés en types "concrets" (en utilisant objectsi le type générique est illimité), en ajoutant des conversions de type et des vérifications de type 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 seulement transformé en code non générique comme en Java.

Cela a plusieurs impacts sur le fonctionnement des génériques dans la pratique. Par exemple:

  • Java doit SomeType<?>vous permettre de transmettre toute 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.
  • Les types génériques non limités en Java signifient que leur valeur est stockée sous forme de fichier object. Cela peut avoir un impact sur les performances lors de l'utilisation de types valeur dans de tels génériques. En C #, lorsque vous utilisez un type valeur dans un type générique, il reste un type valeur.

Pour donner un exemple, supposons que vous ayez un Listtype générique avec un argument générique. En Java, List<String>et List<Int>finira par être exactement du même type au moment de l'exécution - les types génériques n'existent vraiment que pour le code de compilation. Tous les appels à eg GetValueseront respectivement transformés en (String)GetValueet (Int)GetValue.

En C #, List<string>et List<int>sont de deux types différents. Ils ne sont pas interchangeables et leur sécurité de type est également appliquée lors de l'exécution. Peu importe ce que vous faites, new List<int>().Add("SomeString")sera jamais le travail - le stockage sous - jacent List<int>est vraiment une gamme entière, alors qu'en Java, il est nécessairement un objecttableau. En C #, il n'y a pas de lancers impliqués, pas de boxe, etc.

Cela devrait également montrer clairement 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<?>finissent par être exactement du même type. En C #, tous les différents SomeType<T>s spécifiques sont de leur propre type distinct. En supprimant les vérifications à la compilation, il est possible de passer à la SomeType<Int>place de SomeType<String>(et vraiment, tout ce que cela SomeType<?>signifie est "ignorer les vérifications à la compilation pour le type générique donné"). En C #, ce n'est pas possible, même pas pour les types dérivés (c'est-à-dire que vous ne pouvez pas le faire List<object> list = (List<object>)new List<string>();même si stringest 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 pas de sens dans la façon dont les génériques C # fonctionnent.

Luaan
la source
2
Eh bien, vous pouvez utiliser les types List<>, Dictionary<,>et ainsi de suite en C #, mais l'écart entre cela et une liste ou un dictionnaire concret donné demande un peu de réflexion à combler. La variance sur les interfaces aide dans certains des cas où nous aurions pu une fois vouloir combler cet écart facilement, mais pas tous.
Jon Hanna
2
@JonHanna Vous pouvez utiliser List<>pour instancier un nouveau type générique spécifique - mais cela signifie toujours créer le type spécifique que vous voulez. Mais vous ne pouvez pas utiliser List<>comme argument, par exemple. Mais oui, au moins cela vous permet de combler le fossé en utilisant la réflexion.
Luaan
Le .NET Framework a trois contraintes génériques codées en dur qui ne sont pas des types d'emplacement de stockage; toutes les autres contraintes doivent être des types d'emplacement de stockage. En outre, les seuls moments où un type générique Tpeut satisfaire une contrainte de type d'emplacement de stockage Usont quand Tet Usont du même type, ou Uest un type qui peut contenir une référence à une instance de T. Il ne serait pas possible d'avoir un emplacement de stockage de type de manière significative, SomeType<?>mais il serait en théorie possible d'avoir une contrainte générique de ce type.
supercat
1
Il n'est pas vrai que le bytecode Java compilé n'ait aucune notion de générique. C'est juste que les instances de classe n'ont aucune notion de génériques. C'est une différence importante; J'ai déjà écrit à ce sujet sur programmers.stackexchange.com/questions/280169/… , si cela vous intéresse.
ruakh
2

La réification est un concept de modélisation orienté objet.

Reify est un verbe qui signifie "rendre quelque chose d'abstrait réel" .

Lorsque vous effectuez une programmation orientée objet, il est courant de modéliser des objets du monde réel en tant que composants logiciels (par exemple, fenêtre, bouton, personne, banque, véhicule, etc.)

Il est également courant de réifier des concepts abstraits en composants (par exemple, WindowListener, Broker, etc.)

duffymo
la source
2
La réification est un concept général de "rendre quelque chose de réel" qui, bien qu'il s'applique à la modélisation orientée objet, comme vous le dites, a également un sens dans le contexte de la mise en œuvre des génériques.
Jon Hanna
2
J'ai donc été éduqué en lisant ces réponses. Je vais modifier ma réponse.
duffymo
2
Cette réponse ne répond en rien à l'intérêt du PO pour les génériques et le polymorphisme paramétrique.
Erick G.Hagstrom
Ce commentaire ne fait rien pour répondre à l'intérêt de quiconque ni pour booster votre représentant. Je vois que tu n'as rien proposé du tout. La mienne était la première réponse, et elle a défini la réification comme quelque chose de plus large.
duffymo
1
Votre réponse a peut-être été la première, mais vous avez répondu à une question différente, pas à celle posée par le PO, qui aurait été claire à partir du contenu de la question et de ses balises. Peut-être que vous n'avez pas lu la question à fond avant de rédiger votre réponse, ou peut-être que vous ne saviez pas que le terme «réification» a une signification établie dans le contexte des génériques. Quoi qu'il en soit, votre réponse n'est pas utile. Voter contre.
jcsahnwaldt Réintègre Monica le