Как применяются дженерики?

16

Это вопрос с точки зрения внутренних возможностей компилятора.

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

Когда я смотрю на языки без обобщений, это довольно просто, вы можете проверить определение класса, добавить его в иерархию и все.

Но что делать с универсальным классом и, что более важно, как обрабатывать ссылки на него? Как убедиться, что статические поля являются единичными для каждого экземпляра (т.е. каждый раз, когда общие параметры разрешаются).

Допустим, я вижу звонок:

var x = new Foo<Bar>();

Добавить новый Foo_Barкласс в иерархию?


Обновление: пока я нашел только 2 соответствующих сообщения, однако даже они не вдавались в подробности в смысле «как сделать это самостоятельно»:

greenoldman
источник
Upvoting, потому что я думаю, что полный ответ будет интересным. У меня есть некоторые идеи о том, как это работает, но недостаточно, чтобы ответить точно. Я не думаю, что дженерики в C # компилируются в специализированные классы для каждого родового типа. Похоже, что они разрешаются во время выполнения (при использовании дженериков может быть заметна скорость). Может быть, мы можем заставить Эрика Липперта вмешаться?
KChaloux
2
@KChaloux: На уровне MSIL есть одно описание универсального. Когда JIT выполняется, он создает отдельный машинный код для каждого типа значения, используемого в качестве общих параметров, и еще один набор машинного кода, который охватывает все ссылочные типы. Сохранять общее описание в MSIL действительно приятно, потому что оно позволяет создавать новые экземпляры во время выполнения.
Бен Фойгт
@Ben Вот почему я не пытался ответить на вопрос: p
KChaloux,
Я не уверен , если вы все еще вокруг, но на каком языке вы компиляции для . Это будет иметь большое влияние на то, как вы реализуете дженерики. Я могу предоставить информацию о том, как я обычно подходил к нему на переднем конце, но задний конец может сильно отличаться.
Теластин
@Telastyn, по этим темам я уверен :-) Я ищу что-то действительно похожее на C #, в моем случае я собираю на PHP (без шуток). Буду благодарен, если вы поделитесь своими знаниями.
Greenoldman

Ответы:

4

Как убедиться, что статические поля являются единичными для каждого экземпляра (т.е. каждый раз, когда общие параметры разрешаются).

У каждого общего экземпляра есть своя собственная копия (с непонятным названием) MethodTable, в которой хранятся статические поля.

Допустим, я вижу звонок:

var x = new Foo<Bar>();

Добавить новый Foo_Barкласс в иерархию?

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

Но если вы рассматриваете MethodTables, каждый из которых имеет косвенный указатель на свой базовый класс, для формирования этой иерархии, то да, это добавляет новый класс в иерархию.

svick
источник
Спасибо, это интересный кусок. Таким образом, статические поля решаются аналогично виртуальной таблице, верно? Есть ссылка на «глобальный» словарь, который содержит записи для каждого типа? Таким образом, я мог бы использовать 2 сборки, не зная друг друга, Foo<string>и они не будут создавать два экземпляра статического поля Foo.
Гринольдман
1
@greenoldman Ну, не похоже на виртуальный стол, точно так же. MethodTable содержит как статические поля, так и ссылки на методы типа, используемого в виртуальной диспетчеризации (поэтому он называется MethodTable). И да, в CLR должна быть таблица, которую можно использовать для доступа ко всем таблицам MethodTable.
svick
2

Я вижу два реальных конкретных вопроса. Возможно, вы захотите задать дополнительные связанные вопросы (как отдельный вопрос со ссылкой на этот вопрос), чтобы получить полное понимание.

Как статическим полям назначаются отдельные экземпляры для общего экземпляра?

Что ж, для статических членов, которые не связаны с параметрами универсального типа, это довольно легко (используйте словарь, сопоставленный с универсальными параметрами и значением).

Члены (статические или нет), которые связаны с параметрами типа, могут обрабатываться с помощью стирания типа. Просто используйте самое сильное ограничение (часто System.Object). Поскольку информация о типе стирается после проверок типа компилятора, это означает, что проверки типов во время выполнения не понадобятся (хотя преобразования интерфейса все еще могут существовать во время выполнения).

Каждый общий экземпляр появляется отдельно в иерархии типов?

Не в обобщениях .NET. Было принято решение исключить наследование из параметров типа, поэтому оказывается, что все экземпляры универсального объекта занимают одно и то же место в иерархии типов.

Вероятно, это было хорошее решение, потому что невозможность поиска имен из базового класса была бы невероятно удивительной.

Бен Фойгт
источник
Моя проблема в том, что я не могу уйти от мышления с точки зрения шаблона. Например - в отличие от шаблона общий класс будет полностью составлен. Это означает, что в другой сборке, использующей этот класс, что происходит? Уже скомпилированный метод вызывается с внутренним приведением? Я сомневаюсь, что дженерики могут полагаться на ограничение - скорее на аргумент, в противном случае, Foo<int>и Foo<string>будут попадать в те же данные Fooбез ограничений.
Гринольдман
1
@greenoldman: Можем ли мы избежать типов значений на минуту, потому что они на самом деле обрабатываются специально? Если у вас есть List<string>и List<Form>, то, поскольку List<T>внутренне имеет член типа T[]и нет никаких ограничений T, то на самом деле вы получите машинный код, который манипулирует object[]. Однако, поскольку Tв массив помещаются только экземпляры, все, что выходит, может быть возвращено как Tбез дополнительной проверки типа. С другой стороны, если бы вы имели ControlCollection<T> where T : Control, то внутренний массив T[]стал бы Control[].
Бен Фойгт
Правильно ли я понимаю, что ограничение берется и используется как внутреннее имя типа, но когда фактически используется класс, используется приведение? Хорошо, я понимаю эту модель, но у меня сложилось впечатление, что ее использует Java, а не C #.
Гринольдман
3
@greenoldman: Java выполняет стирание типа на этапе преобразования исходного кода в байт-код. Что делает невозможным для верификатора проверить общий код. C # делает это в шаге bytecode-> machine code.
Бен Фойгт
@BenVoigt В Java сохраняется некоторая информация об универсальных типах, так как в противном случае вы не сможете скомпилировать класс, использующий универсальные, без его источника. Он просто не хранится в самой последовательности байт-кода AIUI, а скорее в метаданных класса.
Донал Феллоуз
1

Но что делать с универсальным классом и, что более важно, как обрабатывать ссылки на него?

Общий способ во внешней части компилятора состоит в том, чтобы иметь два вида экземпляров типа: универсальный тип ( List<T>) и связанный универсальный тип ( List<Foo>). Универсальный тип определяет, какие функции существуют, какие поля, и имеет ссылки на универсальный тип, где бы он ни Tиспользовался. Связанный универсальный тип содержит ссылку на универсальный тип и набор аргументов типа. Это имеет достаточно информации для вас, чтобы затем сгенерировать конкретный тип, заменив ссылки на универсальный тип Fooили каковы бы ни были аргументы типа. Такое различие важно, когда вы делаете вывод типа и хотите сделать вывод List<T>против List<Foo>.

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

Что касается серверной части, я действительно не знаю. Вся моя работа с дженериками была ориентирована на CIL в качестве бэкэнда, поэтому я мог скомпилировать их в поддерживаемые дженерики.

Telastyn
источник
Большое спасибо (жаль, что я не могу принять многократные ответы). Приятно слышать, что я в значительной степени сделал этот шаг правильно - в моем случае он List<T>содержит действительный тип (его определение), в то время как List<Foo>(спасибо за часть терминологии) с моим подходом содержат объявления List<T>(конечно, теперь связанные с Fooвместо T).
Greenoldman