Comment les génériques sont-ils mis en œuvre?

16

C'est la question du point de vue interne du compilateur.

Je m'intéresse aux génériques, pas aux modèles (C ++), j'ai donc marqué la question avec C #. Pas Java, car AFAIK les génériques dans les deux langues diffèrent dans les implémentations.

Quand je regarde les langages sans génériques, c'est assez simple, vous pouvez valider la définition de classe, l'ajouter à la hiérarchie et c'est tout.

Mais que faire de la classe générique, et surtout comment gérer les références à celle-ci? Comment s'assurer que les champs statiques sont singuliers par instanciation (c'est-à-dire à chaque fois que les paramètres génériques sont résolus).

Disons que je vois un appel:

var x = new Foo<Bar>();

Dois-je ajouter une nouvelle Foo_Barclasse à la hiérarchie?


Mise à jour: Jusqu'à présent, je n'ai trouvé que 2 messages pertinents, mais même ils n'entrent pas dans les détails dans le sens "comment le faire par vous-même":

greenoldman
la source
Voter parce que je pense qu'une réponse complète serait intéressante. J'ai quelques idées sur la façon dont cela fonctionne, mais pas assez pour répondre avec précision. Je ne pense pas que les génériques en C # se compilent en classes spécialisées pour chaque type générique. Ils semblent être résolus lors de l'exécution (il peut y avoir un coup de vitesse notable en utilisant des génériques). Peut-être que nous pourrons faire jouer Eric Lippert?
KChaloux
2
@KChaloux: Au niveau MSIL, il existe une description du générique. Lorsque le JIT s'exécute, il crée un code machine séparé pour chaque type de valeur utilisé comme paramètres génériques, et un autre ensemble de code machine qui couvre tous les types de référence. La conservation de la description générique dans MSIL est vraiment agréable car elle vous permet de créer de nouvelles instances lors de l'exécution.
Ben Voigt
@Ben C'est pourquoi je n'ai pas essayé de répondre à la question: p
KChaloux
Je ne sais pas si vous êtes toujours là, mais quelle langue vous compilerai à . Cela aura beaucoup d'influence sur la façon dont vous implémentez les génériques. Je peux fournir des informations sur la façon dont je l'ai généralement abordé à l'avant, mais l'arrière peut varier énormément.
Telastyn
@Telastyn, pour ces sujets, je suis sûr :-) Je recherche quelque chose de vraiment proche de C #, dans mon cas je compile en PHP (pas de blague). Je vous serais reconnaissant de partager vos connaissances.
greenoldman

Réponses:

4

Comment s'assurer que les champs statiques sont singuliers par instanciation (c'est-à-dire à chaque fois que les paramètres génériques sont résolus).

Chaque instanciation générique a sa propre copie de la table de méthodes (nommée de manière confuse), qui est où les champs statiques sont stockés.

Disons que je vois un appel:

var x = new Foo<Bar>();

Dois-je ajouter une nouvelle Foo_Barclasse à la hiérarchie?

Je ne suis pas sûr qu'il soit utile de considérer la hiérarchie des classes comme une structure qui existe réellement au moment de l'exécution, c'est plus une construction logique.

Mais si vous considérez MethodTables, chacune avec un pointeur indirect sur sa classe de base, pour former cette hiérarchie, alors oui, cela ajoute une nouvelle classe à la hiérarchie.

svick
la source
Merci, c'est une pièce intéressante. Ainsi, les champs statiques sont résolus de manière similaire à la table virtuelle, non? Il y a une référence au dictionnaire "global" qui contient des entrées pour chaque type? Donc, je pourrais avoir 2 assemblys ne se connaissant pas Foo<string>et ils ne produiront pas deux instances de champ statique à partir de Foo.
greenoldman
1
@greenoldman Eh bien, pas de la même manière que la table virtuelle, exactement la même chose. Le MethodTable contient à la fois des champs statiques et des références à des méthodes du type, utilisées dans la répartition virtuelle (c'est pourquoi il est appelé MethodTable). Et oui, le CLR doit avoir une table qu'il peut utiliser pour accéder à tous les MethodTables.
svick
2

Je vois là deux vraies questions concrètes. Vous voudrez probablement poser des questions connexes supplémentaires (en tant que question distincte avec un lien vers celui-ci) pour bien comprendre.

Comment les champs statiques reçoivent-ils des instances distinctes par instance générique?

Eh bien, pour les membres statiques qui ne sont pas liés aux paramètres de type générique, c'est assez facile (utilisez un dictionnaire mappé des paramètres génériques à la valeur).

Les membres (statiques ou non) liés aux paramètres de type peuvent être gérés via l'effacement de type. Utilisez simplement la contrainte la plus forte (souvent System.Object). Étant donné que les informations de type sont effacées après les vérifications de type du compilateur, cela signifie que les vérifications de type d'exécution ne seront pas nécessaires (bien que les conversions d'interface puissent toujours exister au moment de l'exécution).

Chaque instance générique apparaît-elle séparément dans la hiérarchie de types?

Pas dans les génériques .NET. La décision a été prise d'exclure l'héritage des paramètres de type, il s'avère donc que toutes les instances d'un générique occupent la même place dans la hiérarchie de types.

C'était probablement une bonne décision, car ne pas rechercher des noms dans une classe de base serait incroyablement surprenant.

Ben Voigt
la source
Mon problème est que je ne peux pas m'éloigner de la pensée en termes de modèle. Par exemple - contrairement au modèle, la classe générique est entièrement compilée. Cela signifie que dans un autre assembly utilisant cette classe, que se passe-t-il? La méthode déjà compilée est appelée avec un casting interne? Je doute que les génériques puissent s'appuyer sur la contrainte - plutôt sur l'argument, sinon Foo<int>et Foo<string>frapperaient les mêmes données Foosans contraintes.
greenoldman
1
@greenoldman: Pouvons-nous éviter les types de valeur pendant une minute, car ils sont en fait traités spécialement? Si vous avez List<string>et List<Form>, puisqu'un List<T>membre de type est interne T[]et qu'il n'y a pas de contraintes T, alors vous obtiendrez en fait un code machine qui manipule un object[]. Cependant, comme seules les Tinstances sont placées dans le tableau, tout ce qui sort peut être retourné sous forme de Tsans vérification de type supplémentaire. D'un autre côté, si vous l'aviez ControlCollection<T> where T : Control, alors le tableau interne T[]deviendrait Control[].
Ben Voigt
Dois-je comprendre correctement que la contrainte est prise et utilisée comme nom de type interne, mais lorsque la classe est réellement utilisée, le casting est utilisé? OK, je comprends ce modèle, mais j'avais l'impression que Java l'utilise, pas C #.
greenoldman
3
@greenoldman: Java effectue l'effacement de type dans l'étape de traduction source-> bytecode. Ce qui rend impossible pour le vérificateur de vérifier le code générique. C # le fait dans l'étape bytecode-> code machine.
Ben Voigt
@BenVoigt Certaines informations sont conservées en Java sur les types génériques, sinon vous ne pourriez pas compiler avec une classe utilisant des génériques sans sa source. Il n'est tout simplement pas conservé dans la séquence de bytecode elle-même AIUI, mais plutôt dans les métadonnées de classe.
Donal Fellows
1

Mais que faire de la classe générique, et surtout comment gérer les références à celle-ci?

La manière générale dans le frontal du compilateur est d'avoir deux sortes d'instances de type, le type générique ( List<T>) et un type générique lié ( List<Foo>). Le type générique définit quelles fonctions existent, quels champs et a des références de type générique partout où il Test utilisé. Le type générique lié contient une référence au type générique et un ensemble d'arguments de type. Cela contient suffisamment d'informations pour que vous puissiez ensuite générer un type concret, en remplaçant les références de type génériques par Fooou quels que soient les arguments de type. Ce type de distinction est important lorsque vous faites une inférence de type et que vous devez déduire List<T>par rapport à List<Foo>.

Au lieu de penser à des génériques comme des modèles (qui construisent directement diverses implémentations), il peut être utile de les considérer plutôt comme des constructeurs de types de langage fonctionnel (où les arguments génériques sont comme des arguments dans une fonction qui vous donne un type).

Quant au back-end, je ne sais pas vraiment. Tout mon travail avec les génériques a ciblé CIL comme backend, donc je pouvais les compiler dans les génériques pris en charge.

Telastyn
la source
Merci beaucoup (dommage que je ne puisse pas accepter les réponses multiples). C'est formidable d'entendre que j'ai fait à peu près cette étape correctement - dans mon cas, List<T>détient le type réel (sa définition), tandis que List<Foo>(merci également pour la terminologie) avec mon approche, les déclarations de List<T>(bien sûr maintenant liées à Fooau lieu de T).
greenoldman le