Як реалізуються дженерики?


16

Це питання з точки зору внутрішніх компіляторів.

Мене цікавлять дженерики, а не шаблони (C ++), тому я позначив це питання C #. Не Java, тому що AFAIK генерики в обох мовах відрізняються за реалізацією.

Коли я дивлюся на мови без дженерики, це досить просто, ви можете перевірити визначення класу, додати його до ієрархії, і все.

Але що робити з загальним класом, а ще важливіше, як обробляти посилання на нього? Як переконатися, що статичні поля є сингулярними за моментами (тобто щоразу вирішуються загальні параметри).

Скажімо, я бачу дзвінок:

var x = new Foo<Bar>();

Чи потрібно додати новий Foo_Barклас до ієрархії?


Оновлення: Поки що я знайшов лише 2 відповідні повідомлення, однак навіть вони не вникають у деталі в сенсі "як це зробити самостійно":


Оновлення, тому що я думаю, повна відповідь була б цікавою. У мене є кілька ідей про те, як це працює, але недостатньо, щоб точно відповісти. Я не думаю, що дженерики в C # складаються в спеціалізовані класи для кожного родового типу. Вони, здається, вирішуються під час виконання (може бути помітний швидкий удар від використання дженериків). Може, ми можемо примусити Еріка Ліпперта задзвеніти?
KChaloux

2
@KChaloux: На рівні MSIL є один опис загального. Коли JIT запускається, він створює окремий машинний код для кожного типу значень, що використовується як загальні параметри, і ще один набір машинного коду, який охоплює всі еталонні типи. Збереження загального опису в MSIL дуже приємно, оскільки дозволяє створювати нові екземпляри під час виконання.
Ben Voigt

@Ben Тому я не намагався реально відповісти на питання: p
KChaloux

Я не впевнений , якщо ви все ще навколо, але на якій мові ви компіляції для . Це матиме великий вплив на те, як ви реалізуєте дженерики. Я можу надати інформацію про те, як я зазвичай підходив до нього на передньому кінці, але задній кінець може сильно відрізнятися.
Теластин

@Telastyn, для цих тем я впевнений :-) Я шукаю щось дійсно близьке до C #, в моєму випадку я перекладаю на PHP (не жартую). Буду вдячний, якщо ви поділитесь своїми знаннями.
greenoldman

Відповіді:


4

Як переконатися, що статичні поля є сингулярними за моментами (тобто щоразу вирішуються загальні параметри).

Кожна загальна інстанція має свою копію (збиття з пантелику) MethodTable, де зберігаються статичні поля.

Скажімо, я бачу дзвінок:

var x = new Foo<Bar>();

Чи потрібно додати новий Foo_Barклас до ієрархії?

Я не впевнений, що корисно думати про ієрархію класів як про якусь структуру, яка існує насправді під час виконання, це скоріше логічна конструкція.

Але якщо ви розглядаєте MethodTables, кожен з непрямим покажчиком на свій базовий клас, для формування цієї ієрархії, то так, це додає новий клас до ієрархії.


Дякую, це цікава деталь. Тож статичні поля вирішуються аналогічно віртуальній таблиці, правда? Чи є посилання на "глобальний" словник, який містить записи для кожного типу? Тому я міг би мати 2 збірки, не знаючи один про одного, Foo<string>і вони не будуть створювати два екземпляри статичного поля Foo.
greenoldman

1
@greenoldman Ну, не так, як у віртуальній таблиці, точно так само. MethodTable містить як статичні поля, так і посилання на методи типу, що використовуються у віртуальній розсилці (саме тому вона називається MethodTable). І так, CLR повинен мати таблицю, яку він може використовувати для доступу до всіх таблиць MethodTables.
svick

2

Я бачу два фактичних конкретні питання. Напевно, ви хочете задати додаткові відповідні запитання (як окреме запитання із посиланням на це), щоб отримати повне розуміння.

Як статичним полям даються окремі екземпляри на загальний екземпляр?

Що ж, для статичних членів, які не пов'язані з параметрами загального типу, це досить просто (використовуйте словник, зіставлений із загальних параметрів на значення).

Члени (статичні чи ні), пов'язані з параметрами типу, можуть оброблятися за допомогою стирання типу. Просто використовуйте будь-яке найсильніше обмеження (часто System.Object). Оскільки інформація про тип стирається після перевірки типу компілятора, це означає, що перевірки типу виконання не будуть потрібні (хоча касти інтерфейсу все ще можуть існувати під час виконання).

Чи кожен загальний екземпляр відображається окремо в ієрархії типів?

Не в .NET generics. Було прийнято рішення про виключення спадкування з параметрів типу, тому виявляється, що всі екземпляри родового походження займають одне і те ж місце в ієрархії типів.

Це, мабуть, було гарним рішенням, оскільки невиконання пошуку імен з базового класу було б неймовірно дивовижним.


Моя проблема полягає в тому, що я не можу відійти від мислення з точки зору шаблону. Наприклад - в відміну від шаблону загальний клас буде повністю складений. Це означає, що в інших зборах, що використовують цей клас, що відбувається? Уже складений метод називається внутрішнім кастингом? Я сумніваюся, що дженеріки можуть покладатися на обмеження, а не на аргумент, інакше Foo<int>і Foo<string>вдарять ті самі дані Fooбез обмежень.
greenoldman

1
@greenoldman: Чи можемо ми уникнути типових значень за хвилину, оскільки вони насправді обробляються спеціально? Якщо в вас є List<string>та List<Form>, то , так як List<T>всередині є елемент типу T[]і немає ніяких обмежень на T, то , що ви на самому справі отримуєте машинний код , який маніпулює object[]. Однак, оскільки Tв масив вводяться лише екземпляри, все, що виходить, може бути повернуто як Tбез додаткової перевірки типу. З іншого боку, якби у вас був ControlCollection<T> where T : Control, то внутрішній масив T[]став би Control[].
Ben Voigt

Я правильно розумію, що обмеження приймається і використовується як внутрішнє ім'я типу, але коли клас використовується фактично, використовується кастинг? Гаразд, я розумію цю модель, але я був під враженням, що Java використовує її, а не C #.
greenoldman

3
@greenoldman: Java виконує стирання типу на етапі трансляції вихідного коду. Що унеможливлює перевірку верифікатора загального коду. C # робить це в кроці байт-коду-> машинного коду.
Ben Voigt

@BenVoigt На Java зберігається деяка інформація про загальні типи, оскільки в іншому випадку ви не змогли скласти проти класу, що використовує загальний характер, без його джерела. Він просто не зберігається в послідовності байт-коду AIUI, а швидше у метаданих класу.
стипендіати Дональ

1

Але що робити з загальним класом, а ще важливіше, як обробляти посилання на нього?

Загальний спосіб у передньому кінці компілятора - це мати два типи екземплярів типу, загальний тип ( List<T>) та прив'язаний загальний тип ( List<Foo>). Загальний тип визначає, які функції існують, які поля та має загальні посилання типу, де б вони Tне використовувались. Зв'язаний загальний тип містить посилання на загальний тип та набір аргументів типу. Це має достатньо інформації для того, щоб потім генерувати конкретний тип, замінюючи загальні посилання типу Fooабо аргументів типу. Цей вид розрізнення важливий, коли ви робите висновок про тип і потрібно робити висновок List<T>проти List<Foo>.

Замість того, щоб мислити генеричні типи, як шаблони (які створюють різні реалізації безпосередньо), може бути корисним замість того, щоб думати про них, як конструктори функціональних мовних типів (де загальні аргументи - це як аргументи у функції, яка дає вам тип).

Щодо заднього кінця, я насправді не знаю. Уся моя робота з дженериками орієнтована на CIL як бекенд, тому я міг зібрати їх у підтримувані там дженерики.


Дуже дякую (шкода, що не можу приймати багаторазові відповіді). Це чудово чути, що я зробив досить багато цього кроку правильно - в моєму випадку відповідає List<T>справжньому типу (його визначення), тоді як List<Foo>(спасибі також за термінологічний фрагмент) з моїм підходом тримаю декларації List<T>(звичайно, тепер це пов'язано з Fooзамість T).
greenoldman
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.