13 votes

Quelle est la mise en œuvre de la générique pour le Common Language Runtime de NET ?

Lorsque vous utilisez des collections génériques en C# (ou en .NET en général), le compilateur fait-il le travail que les développeurs avaient l'habitude de faire pour créer une collection générique pour un type spécifique ? En gros, cela nous épargne du travail ?

Maintenant que j'y pense, ce n'est pas possible. Parce que sans les génériques, nous devions faire des collections qui utilisaient un tableau non générique en interne, et donc il y avait des boîtes et des boîtes (s'il s'agissait d'une collection de types de valeurs), etc.

Alors, comment les génériques sont-ils rendus en CIL ? Que fait-il pour impliquer lorsque nous disons que nous voulons une collection générique de quelque chose ? Je ne veux pas nécessairement des exemples de code CIL (bien que ce serait bien), je veux connaître les concepts de la façon dont le compilateur prend nos collections génériques et les rend.

Merci de votre attention !

P.S. Je sais que je pourrais utiliser ildasm pour examiner cela, mais le CIL me semble toujours être du chinois, et je ne suis pas prêt à m'y attaquer. Je veux juste savoir comment C# (et d'autres langages je suppose aussi) rend en CIL pour gérer les génériques.

16voto

Peter Huene Points 4454

Pardonnez mon billet verbeux, mais ce sujet est très vaste. Je vais tenter de décrire ce que le compilateur C# émet et comment cela est interprété par le compilateur JIT au moment de l'exécution.

ECMA-335 (il s'agit d'un document de conception très bien écrit ; jetez-y un coup d'œil) est le meilleur moyen de savoir comment tout, et je dis bien tout, est représenté dans un assemblage .NET. Il existe quelques tables de métadonnées CLI pour les informations génériques d'un assemblage :

  1. GenericParam - Stocke les informations relatives à un paramètre générique (index, drapeaux, nom, type/méthode propriétaire).
  2. GenericParamConstraint - Stocke les informations relatives à une contrainte de paramètre générique (paramètre générique propriétaire, type de contrainte).
  3. MethodSpec - Stocke les signatures de méthodes génériques instanciées (par exemple Bar.Method<int> pour Bar.Method<T>).
  4. TypeSpec - Stocke les signatures de types génériques instanciés (par exemple Bar<int> pour Bar<T>).

C'est dans cet esprit que nous allons présenter un exemple simple utilisant cette classe :

class Foo<T>
{
    public T SomeProperty { get; set; }
}

Lorsque le compilateur C# compile cet exemple, il définit Foo dans la table de métadonnées TypeDef, comme il le ferait pour n'importe quel autre type. Contrairement à un type non générique, il aura également une entrée dans la table GenericParam qui décrira son paramètre générique (index = 0, flags = ?, name = (index into String heap, "T"), owner = type "Foo").

L'une des colonnes de données du tableau TypeDef est l'indice de départ du tableau MethodDef, qui contient la liste continue des méthodes définies pour ce type. Pour Foo, nous avons défini trois méthodes : un getter et un setter pour SomeProperty et un constructeur par défaut fourni par le compilateur. Par conséquent, le tableau MethodDef contiendra une ligne pour chacune de ces méthodes. L'une des colonnes importantes du tableau MethodDef est la colonne "Signature". Cette colonne contient une référence à un bloc d'octets qui décrit la signature exacte de la méthode. L'ECMA-335 décrit en détail ces blobs de signature de métadonnées, je ne vais donc pas régurgiter ces informations ici.

Le blob de la signature de la méthode contient des informations sur le type des paramètres ainsi que sur la valeur de retour. Dans notre exemple, le setter prend un T et le getter renvoie un T. Alors, qu'est-ce qu'un T ? Dans le blob de signature, il s'agit d'une valeur spéciale qui signifie "le paramètre de type générique à l'index 0". Cela signifie la ligne dans le tableau GenericParams qui a l'index=0 avec owner=type "Foo", qui est notre "T".

Il en va de même pour l'auto-property backing store field. L'entrée de Foo dans la table TypeDef aura un index de départ dans la table Field et la table Field a une colonne "Signature". La signature du champ indiquera que le type du champ est "le paramètre de type générique à l'index 0".

C'est très bien, mais où la génération de code entre-t-elle en jeu lorsque T est de types différents ? C'est en fait la responsabilité du compilateur JIT de générer le code pour les instanciations génériques et non celle du compilateur C#.

Prenons un exemple :

Foo<int> f1 = new Foo<int>(); 
f1.SomeProperty = 10;
Foo<string> f2 = new Foo<string>();
f2.SomeProperty = "hello";

La compilation donnera quelque chose comme ce CIL :

newobj <MemberRefToken1> // new Foo<int>()
stloc.0 // Store in local "f1"
ldloc.0 // Load local "f1"
ldc.i4.s 10 // Load a constant 32-bit integer with value 10
callvirt <MemberRefToken2> // Call f1.set_SomeProperty(10)
newobj <MemberRefToken3> // new Foo<string>()
stloc.1 // Store in local "f2"
ldloc.1 // Load local "f2"
ldstr <StringToken> // Load "hello" (which is in the user string heap)
callvirt <MemberRefToken4> // Call f2.set_SomeProperty("hello")

Qu'est-ce que c'est que cette histoire de MemberRefToken ? Un MemberRefToken est un jeton de métadonnées (les jetons sont des valeurs de quatre octets dont l'octet le plus significatif est un identifiant de table de métadonnées et les trois octets restants sont le numéro de ligne, basé sur 1) qui fait référence à une ligne de la table de métadonnées MemberRef. Cette table stocke une référence à une méthode ou à un champ. Avant les génériques, c'est la table qui stockait les informations sur les méthodes/champs que vous utilisiez à partir des types définis dans les assemblages référencés. Cependant, elle peut également être utilisée pour référencer un membre dans une instanciation générique. Disons donc que MembreRefToken1 fait référence à la première ligne du tableau MemberRef. Elle peut contenir les données suivantes : class = TypeSpecToken1 , name = ".ctor", blob = <référence à la signature blob attendue de .ctor>.

TypeSpecToken1 ferait référence à la première ligne du tableau TypeSpec. D'après ce qui précède, nous savons que cette table stocke les instanciations des types génériques. Dans ce cas, cette ligne contiendrait une référence à un blob de signature pour "Foo<int>". Ainsi, cette MembreRefToken1 dit en réalité que nous faisons référence à "Foo<int>.ctor()".

MembreRefToken1 y MembreRefToken2 partageraient la même valeur de classe, c'est-à-dire TypeSpecToken1 . Ils diffèrent cependant sur le nom et la signature ( MéthodeRefToken2 serait pour "set_SomeProperty"). De même, MembreRefToken3 y MembreRefToken4 partagerait TypeSpecToken2 Les deux autres types d'objets, les "Foo<string>", l'instanciation de "Foo<string>", diffèrent de la même manière en ce qui concerne le nom et le "blob".

Lorsque le compilateur JIT compile le CIL ci-dessus, il remarque qu'il voit une instanciation générique qu'il n'a jamais vue auparavant (c'est-à-dire Foo<int> ou Foo<string>). Ce qui se passe ensuite est assez bien couvert par la réponse de Shiv Kumar, je ne vais donc pas la répéter en détail ici. Pour faire simple, lorsque le compilateur JIT rencontre un nouveau type générique instancié, il peut émettre un tout nouveau type dans son système de types avec une disposition des champs utilisant les types réels dans l'instanciation à la place des paramètres génériques. Ils auraient également leurs propres tables de méthodes et la compilation JIT de chaque méthode impliquerait le remplacement des références aux paramètres génériques par les types réels de l'instanciation. Il incombe également au compilateur JIT d'assurer l'exactitude et la vérifiabilité du CIL.

En résumé : Le compilateur C# émet des métadonnées décrivant ce qui est générique et comment les types/méthodes génériques sont instanciés. Le compilateur JIT utilise ces informations pour créer de nouveaux types (en supposant qu'ils ne soient pas compatibles avec une instanciation existante) au moment de l'exécution pour les types génériques instanciés et chaque type aura sa propre copie du code qui a été compilé JIT sur la base des types réels utilisés dans l'instanciation.

J'espère que cela a eu un peu de sens.

10voto

Shiv Kumar Points 5939

Pour les types de valeurs, une "classe" spécifique est définie au moment de l'exécution pour chaque classe générique de type de valeur. Pour les types de référence, il n'y a qu'une seule définition de classe qui est réutilisée pour les différents types.

Je simplifie, mais c'est le concept.

Conception et mise en œuvre de produits génériques pour la NET Common Language Runtime

O Lorsque l'exécution requiert une instanciation particulière d'un instanciation d'une classe paramétrée paramétrée, le chargeur vérifie si la classe insta qu'il a vu auparavant ; si ce n'est pas le cas, alors une disposition des champs est déterminée et une nouvelle [ ] entre toutes les instanciations compatibles. Les éléments de cette table sont des entrées pour les méthodes de la classe. Lorsque ces stubs sont invoqués ultérieurement, ils génèreront ("juste à temps") code à partager pour toutes les instanciations compatibles. [ ] invocation d'une méthode polymorphe (non virtuelle) (non virtuelle) à une instanciation particulière, nous vérifions d'abord que

si nous avons compilé un tel appel avant pour une instanciation compatible ; si Si ce n'est pas le cas, un talon d'entrée est généré, qui générera à son tour du code à partager partagé pour toutes les compatibles. Deux instanciations sont compatibles si, pour toute classe paramétrée sa compilation à ces instanciations donne lieu à un code identiques et d'autres structures d'exécution (par exemple, la disposition des champs et les tables GC), à l'exception des dictionnaires décrits ci-dessous dans la section 4.4. En particulier, tous les types de référence sont compatibles entre eux, car le chargeur et le compilateur et le compilateur JIT ne font aucune distinction aux fins de la disposition des champs ou de la génération de code. En ce qui concerne l'implémentation pour Intel x86, au moins, les types primitifs sont mutuellement incompatibles, même les types même s'ils ont la même taille (les floats et les [ ] paramètres). Il reste donc les qui sont compatibles si leur disposition est la même en ce qui concerne du point de vue du ramassage des ordures, c'est-à-dire qu'ils partagent le même schéma de pointeurs tracés.

http://research.microsoft.com/pubs/64031/designandimplementationofgenerics.pdf

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