Что такое реализация генериков для NET Common Language Runtime

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

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

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

Спасибо!

P.S. Я знаю, что я мог бы использовать ildasm, чтобы посмотреть на это, но я CIL по-прежнему выглядит как китайский для меня, и я не готов это решать. Я просто хочу, чтобы понятия С# (и других языков, которые, как я полагаю, тоже) отображали в CIL для обработки дженериков.

Ответы

Ответ 1

Простите мой многословный пост, но эта тема довольно широкая. Я попытаюсь описать, что испускает компилятор С# и как это интерпретируется компилятором JIT во время выполнения.

ECMA-335 (это действительно хорошо написанный проектный документ, проверьте его), где он за то, что знает, как все, и я имею в виду все, представлено в сборке .NET. В сборке есть несколько связанных таблиц метаданных CLI для общей информации:

  • GenericParam - хранит информацию об общем параметре (индекс, флаги, имя, тип/метод).
  • GenericParamConstraint - хранит информацию об общем ограничении параметров (владеющем общим параметром, типом ограничения).
  • MethodSpec - хранит экземпляры генерируемых стандартных сигнатур (например, Bar.Method <int> для Bar.Method <T> ).
  • TypeSpec - хранит экземпляры типичного типа (например, Bar <int> для Bar <T> ).

Поэтому, имея в виду это, пропустите простой пример с помощью этого класса:

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

Когда компилятор С# компилирует этот пример, он определит Foo в таблице метаданных TypeDef, как и для любого другого типа. В отличие от не общего типа, он также будет иметь запись в таблице GenericParam, которая будет описывать свой общий параметр (index = 0, flags =?, Name = (индекс в кучу String, "T" ), owner = type "Foo" ).

Одним из столбцов данных в таблице TypeDef является начальный индекс в таблице MethodDef, который является непрерывным списком методов, определенных для этого типа. Для Foo мы определили три метода: getter и setter для SomeProperty и конструктор по умолчанию, предоставленный компилятором. В результате таблица MethodDef будет содержать строку для каждого из этих методов. Одним из важных столбцов в таблице MethodDef является столбец "Подпись". В этом столбце хранится ссылка на байк байтов, который описывает точную подпись метода. ECMA-335 подробно рассказывает об этих блоках подписи метаданных, поэтому я не буду срывать эту информацию здесь.

Ключ-подпись функции содержит информацию о параметрах, а также возвращаемое значение. В нашем примере сеттер принимает T, а getter возвращает T. Ну, что же такое T? В блоке подписи это будет специальное значение, которое означает "параметр типового типа с индексом 0". Это означает, что строка в таблице GenericParams имеет индекс = 0 с owner = type "Foo", который является нашим "T".

То же самое касается поля хранилища авто-свойств. Запись Foo в таблице TypeDef будет содержать начальный индекс в таблице Field, а в таблице Field будет столбец "Подпись". Подпись поля будет означать, что тип поля - это "параметр типового типа с индексом 0".

Это хорошо и хорошо, но где генерируется генерация кода, когда T - разные типы? На самом деле ответственность компилятора JIT заключается в генерации кода для генерических экземпляров, а не в компиляторе С#.

Посмотрим на пример:

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

Это скомпилирует что-то вроде этого 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")

Итак, что это за бизнес MemberRefToken? MemberRefToken - это токен метаданных (токены - это четыре байтовых значения с самым значимым байтом, являющимся идентификатором таблицы метаданных, а остальные три байта - номером строки, 1), который ссылается на строку в таблице метаданных MemberRef. В этой таблице содержится ссылка на метод или поле. Перед generics это таблица, в которой будет храниться информация о методах/полях, которые вы используете, из типов, определенных в ссылочных сборках. Тем не менее, он также может использоваться для ссылки на элемент в общем экземпляре. Поэтому скажем, что MemberRefToken1 относится к первой строке в таблице MemberRef. Он может содержать эти данные: class= TypeSpecToken1, name = ".ctor", blob = < ссылка на ожидаемый заголовок подписи .ctor > .

TypeSpecToken1 будет ссылаться на первую строку в таблице TypeSpec. Сверху мы знаем, что в этой таблице хранятся экземпляры родовых типов. В этом случае эта строка будет содержать ссылку на подпись blob для "Foo <int> ". Таким образом, этот MemberRefToken1 действительно говорит, что мы ссылаемся на "Foo <int> .ctor()".

MemberRefToken1 и MemberRefToken2 будет использовать одно и то же значение класса, то есть TypeSpecToken1, Однако они будут отличаться от имени и подписи blob ( MethodRefToken2 для "set_SomeProperty" ). Аналогично, MemberRefToken3 и MemberRefToken4 будет делиться TypeSpecToken2, создание "Foo <string> ", но отличается от имени и blob в том же путь.

Когда компилятор JIT компилирует вышеупомянутый CIL, он замечает, что он видит общий экземпляр, который он не видел раньше (т.е. Foo <int> или Foo <string> ). То, что происходит дальше, довольно хорошо освещено ответом Шива Кумара, поэтому я не буду повторять его подробно здесь. Проще говоря, когда компилятор JIT сталкивается с новым типизированным типичным типом, он может испускать совершенно новый тип в свою систему типов с полевой компоновкой, используя фактические типы в экземпляре вместо общих параметров. У них также были бы свои собственные таблицы методов, а компиляция JIT каждого метода включала бы замену ссылок на общие параметры на фактические типы из экземпляра. Это также ответственность компилятора JIT для обеспечения правильности и проверяемости CIL.

Итак, чтобы подвести итог: компилятор С# испускает метаданные, описывающие, что общего и как генерируются типичные типы/методы. Компилятор JIT использует эту информацию для извлечения новых типов (при условии, что он несовместим с существующим экземпляром) во время выполнения для экземпляров генерируемых типов, и каждый тип будет иметь свою собственную копию кода, который был JIT, скомпилированный на основе используемых фактических типов в экземпляре.

Надеюсь, это имело смысл некоторым образом.

Ответ 2

Для типов значений существует определенный "класс", определенный во время выполнения для каждого класса универсального типа значений. Для ссылочных типов существует только одно определение класса, которое повторно используется для разных типов.

Я упрощаю здесь, но это понятие.

Разработка и внедрение общих NET Common Language Runtime

Наша схема выполняется примерно так: Когда время выполнения требует определенного создание параметризованной класса, загрузчик проверяет, экземпляр совместим с любым что он видел раньше; если нет, то определяется макет поля и vtable создается, чтобы быть общим между всеми совместимыми экземплярами. Элементы в этой таблице vtable заглушки для методов класса. Когда эти заглушки будут позже вызваны, они будут генерировать ( "точно вовремя" ) код для совместного использования для всех совместимых конкретизации. При компиляции вызов (не виртуального) полиморфного метода в конкретном экземпляр, мы сначала проверяем, чтобы увидеть

если мы скомпилировали такой вызов раньше для некоторого совместимого экземпляра; если нет, тогда создается заглушка записи, который, в свою очередь, будет генерировать код общий для всех совместимых конкретизации. Два экземпляра совместим, если для любого параметризованного класс его компиляции при этих экземпляры дают одинаковые код и другие исполнительные структуры (например, расположение полей и таблицы GC), кроме описанных словарей ниже в разделе 4.4. В частности, все ссылочные типы совместимы друг с другом, потому что загрузчик и JIT-компилятор не делают различий для целей размещения полей или генерации кода. О реализации для Intel x86, по крайней мере, примитивный типы взаимно несовместимы, даже если они имеют одинаковые размеры (цветки и ints имеют разные параметры конвенции). Это оставляет определяемым пользователем типы структур, которые совместимы, если их расположение одинаково с уважением к сборке мусора, то есть они делят тот же шаблон прослеживаемых указателей.

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