Подтвердить что ты не робот

Как реализуется вызов виртуального общего метода?

Мне интересно, как CLR реализует такие вызовы:

abstract class A {
    public abstract void Foo<T, U, V>();
}

A a = ...
a.Foo<int, string, decimal>(); // <=== ?

Является ли этот вызов причиной какого-либо поиска хеш-карт по типу параметров токенов в качестве ключей и скомпилированной универсальной специализации метода (по одному для всех ссылочных типов и разного кода для всех типов значений) в качестве значений?

4b9b3361

Ответ 1

Я не нашел много точной информации об этом, поэтому большая часть этого ответа основана на превосходной статье о продуктах .Net с 2001 года (даже раньше .Net 1.0 вышел!), Одно короткое замечание в последующем документе и то, что я собрал из исходного кода SSCLI v. 2.0 (хотя я не смог найти точный код для вызова виртуальных общих методов).

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

Теперь, как называется не общий виртуальный метод? Подобно полиморфизму в таких языках, как С++, существует таблица методов, доступная из указателя this (ссылка). У каждого производного класса есть своя таблица методов и его методы. Таким образом, чтобы вызвать виртуальный метод, получите ссылку на this (переданный как параметр), оттуда, получите ссылку на таблицу методов, посмотрите на правильную запись в ней (номер записи является константой для конкретной функции ) и вызовите код, на который указывает точка входа. Методы вызова через интерфейсы немного сложнее, но не интересны для нас сейчас.

Теперь нам нужно знать о совместном использовании кода. Код может быть разделен между двумя "экземплярами" одного и того же метода, если ссылочные типы в параметрах типа соответствуют любым другим ссылочным типам, а типы значений точно такие же. Например, C<string>.M<int>() использует код с C<object>.M<int>(), но не с C<string>.M<byte>(). Нет никакой разницы между параметрами типа и параметрами типа метода. (В оригинальной статье 2001 года упоминается, что код может использоваться совместно, когда оба параметра struct имеют одинаковый макет, но я не уверен, что это истинно в реальной реализации.)

Сделайте промежуточный шаг на пути к общим методам: не общие методы в родовых типах. Из-за совместного использования кода нам нужно получить параметры типа где-нибудь (например, для вызова кода типа new T[]). По этой причине каждое создание типичного типа (например, C<string> и C<object>) имеет свой собственный дескриптор типа, который содержит параметры типа, а также таблицу методов. Обычные методы могут получить доступ к дескриптору этого типа (технически структура, смутно называемая MethodTable, хотя она содержит больше, чем просто таблицу методов) из справочника this. Существует два типа методов, которые не могут этого сделать: статические методы и методы для типов значений. Для них дескриптор типа передается как скрытый аргумент.

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

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

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

object Lift<T>(int count) where T : new()
{
    if (count == 0)
        return new T();

    return Lift<List<T>>(count - 1);
}

Эквивалентный код С++ заставляет компилятор отказаться от.

Ответ 2

Да. Код для определенного типа создается в время выполнения с помощью CLR и сохраняет хэш-таблицу (или аналогичную) реализаций.

Страница 372 из CLR через С#:

Когда метод, который использует общий тип параметры JIT-компилируются, CLR принимает метод IL, заменяет аргументы заданного типа, а затем создает собственный код, который является специфическим к этому методу, действующему на определенных типов данных. Это точно что вы хотите и является одним из основных особенности дженериков. Однако там является недостатком: CLR сохраняет генерация собственного кода для каждого метод/тип комбинация. Это называемый взрывом кода. Эта может привести к увеличению рабочий набор приложений существенно, тем самым повреждая представление. К счастью, в CLR есть оптимизации, встроенные в нее, чтобы уменьшить взрыв кода. Во-первых, если метод вызываемый для определенного аргумента типа, и позже метод снова вызван используя аргумент того же типа, CLR будет компилировать код для этого метод/тип комбинация только один раз. So если одна сборка использует List, и совершенно другая сборка (загружается в тот же AppDomain) также использует List, CLR будет скомпилировать методы для списка только раз. Это уменьшает взрыв кода по существу.

Ответ 3

ИЗМЕНИТЬ

Теперь я наткнулся на то, что я натолкнулся на https://msdn.microsoft.com/en-us/library/sbh15dya.aspx, в котором четко указано, что генерики при использовании ссылочных типов повторно используют один и тот же код, таким образом, я согласился бы с этим в качестве окончательного авторитета.

ОРИГИНАЛЬНЫЙ ОТВЕТ

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

Во-первых, Clr через С# Джеффри Рихтера, опубликованного Microsoft Press, является таким же достоверным, как блог msdn, тем более что блог уже устарел (для большего количества книг от него посмотрите http://www.amazon.com/Jeffrey-Richter/e/B000APH134 нужно согласиться с тем, что он эксперт по окнам и .net).

Теперь позвольте мне сделать свой собственный анализ.

Очевидно, что два общих типа, которые содержат разные аргументы ссылочного типа, не могут использовать один и тот же код

Например, List <TypeA> и List < TypeB → не может использовать один и тот же код, поскольку это может привести к добавлению объекта TypeA в List <TypeB> через отражение, а clr также строго типизирован на генетике (в отличие от Java, в которой только компилятор проверяет общий, но базовая JVM не имеет понятия о них).

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

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

Другая проблема, которая возникла бы при повторном использовании кода, поскольку операторы is и as более не работают корректно, и в целом все типы кастингов будут иметь серьезные проблемы.

СЕЙЧАС НА АКТУАЛЬНОЕ ИСПЫТАНИЕ:

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

EDIT:

Смотрите http://blogs.msdn.com/b/csharpfaq/archive/2004/03/12/how-do-c-generics-compare-to-c-templates.aspx о том, как он реализован:

Использование пространства

Использование пространства отличается между С++ и С#. Потому что С++ шаблоны выполняются во время компиляции, каждое использование другого типа в шаблон создает отдельный фрагмент кода, создаваемый компилятор.

В мире С# это несколько отличается. Фактические реализации используя определенный тип, создаются во время выполнения. Когда среда выполнения создает такой тип, как List, JIT увидит, что это уже было создано. Если это так, это просто пользователи, которые кодируют. Если нет, это займет IL, что компилятор сгенерировал и выполнил соответствующие замены с фактическим типом.

Это не совсем правильно. Существует отдельный собственный код кода для каждый тип значения, но поскольку ссылочные типы имеют все значения, они могут поделиться своей реализацией.

Это означает, что подход С# должен иметь меньший размер диск и в памяти, так что преимущество для дженериков над С++ шаблоны.

Фактически, С++-компоновщик реализует функцию, известную как "шаблон folding", где компоновщик ищет собственные секторы кода, которые идентичны, и если он их находит, складывает их вместе. Так что это не ясно, как бы казалось.

Как видно, CLR "может" повторно использовать реализацию для ссылочных типов, как и текущие компиляторы С++, однако на этом нет никакой гарантии, а для небезопасного кода с использованием stackalloc и указателей это, вероятно, не так, и там могут быть и другие ситуации.

Однако нам нужно знать, что в системе типа CLR они рассматриваются как разные типы, такие как разные вызовы статических конструкторов, отдельные статические поля, отдельные объекты типа, а объект типа аргумента T1 не должен быть может получить доступ к частному полю другого объекта с аргументом типа T2 (хотя для объекта того же типа действительно возможно получить доступ к закрытым полям из другого объекта того же типа).