web-dev-qa-db-fra.com

Qu'est-ce que la réification?

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.

  • 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/valeur est réifié?
155
Martijn

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) .

    1. En tant qu'effet secondaire de cette technique d'implémentation, les seuls arguments de type génériques autorisés de manière native sont les types pouvant 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 des types value comme arguments de type générique nécessite leur mise en boîte (en les plaçant dans un wrapper de type référence simple).
    2. Aucun code n'est dupliqué afin de mettre en œuvre les génériques de cette façon.
    3. Les informations de type qui auraient pu être disponibles au moment de l'exécution (à l'aide de la réflexion) sont perdues. Cela signifie à son tour 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 support de l'environnement d'exécution.
    5. Il existe quelques solutions permettant de conserver les informations de type qu'un 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 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 .

    1. Cette technique de mise en oeuvre permet d'instancier n'importe quel type de combinaison d'arguments de type. L'utilisation de types de valeur en tant qu'arguments de type génériques ne provoque pas de boxe, 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 pourrait être un problème - mais en pratique pas, 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é, car une définition de type générique est compilée avant que toute réification ne se produise (ceci est fait par - compilation de la définition par rapport aux contraintes sur les paramètres de type - donc, le compilateur doit être capable de "comprendre" la définition même en l'absence d'arguments de type spécifiques ).
    4. Cette technique d'implémentation dépend fortement du support d'exécution et de la compilation JIT (c'est pourquoi vous entendez souvent cela les génériques C # ont des 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 souhaitez 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 vous-même, à l'aide de la commande 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 .

    1. Cette technique de mise en oeuvre permet d'instancier n'importe quel type de combinaison d'arguments de type.
    2. On sait que cela duplique du code binaire, mais une chaîne d’outils suffisamment intelligente pourrait encore le détecter et partager du code pour certaines instanciations.
    3. La définition de modèle elle-même n'est pas "compilée" - seules ses instanciations concrètes sont réellement compilées . Cela place moins de contraintes sur le compilateur et permet un plus grand degré de spécialisation de modèles .
    4. Comme les instanciations de modèles 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é monomorphization , en particulier dans la Rust communauté.). Word est utilisé contrairement à polymorphisme paramétrique , qui est le nom du concept à l'origine des génériques.
196

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).

26
Jon Hanna

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:

  • Java a 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.
  • Les types génériques non liés dans Java signifient que leur valeur est stockée sous la forme 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 #.

14
Luaan

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.).

2
duffymo