Как генераторы С#/.NET знают свои типы параметров?

В С# общая функция или класс осведомлены о типах ее общих параметров. Это означает, что доступна информация о динамическом типе, например is или as (в отличие от Java, где это не так).

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

Если дженерики сохраняются на уровне IL, и я считаю, что это так, то я хотел бы знать, как это делается на этом уровне.

Ответ 1

Поскольку вы отредактировали свой вопрос, чтобы распространить его вне компилятора С# на компилятор JIT, рассмотрим этот процесс, взяв List<T> в качестве нашего примера.

Как мы установили, существует только одно IL-представление класса List<T>. Это представление имеет параметр типа, соответствующий параметру типа T, показанному в коде С#. Как говорит Хольгер Тиманн в своем комментарии, когда вы используете класс List<> с заданным аргументом типа, компилятор JIT создает представление этого класса для этого аргумента с собственным кодом.

Однако для ссылочных типов он компилирует собственный код только один раз и повторно использует его для всех других ссылочных типов. Это возможно, потому что в системе виртуального исполнения (VES, обычно называемой "runtime" ) существует только один ссылочный тип, называемый O в спецификации (см. Параграф I.12.1, таблица I.6, в стандартном: http://www.ecma-international.org/publications/standards/Ecma-335.htm). Этот тип определяется как "ссылка на объект собственного размера на управляемую память".

Другими словами, все объекты в (виртуальном) стеке оценки VES представлены "ссылкой на объект" (фактически указателем), которая, взятая сама по себе, по сути является беспричинной. Как же VES гарантирует, что мы не будем использовать членов несовместимого типа? Что мешает нам вызвать свойство string.Length в экземпляре System.Random?

Для обеспечения безопасности типов VES использует метаданные, которые описывают статический тип каждой ссылки на объект, сравнивая тип приемника вызова метода с типом, идентифицированным токеном метаданных метода (это относится также к доступу к другим типам-членам).

Например, чтобы вызвать метод класса объекта, ссылка на объект должна находиться в верхней части виртуального пакета оценки. Статический тип этой ссылки известен благодаря метаданных метода и анализу "перехода стека" - изменениям состояния стека, вызванным каждой командой IL. Затем команда call или callvirt указывает метод, который будет вызываться, включая маркер метаданных, представляющий этот метод, который, конечно, указывает тип, на котором определяется метод.

VES "проверяет" код перед его компиляцией, сравнивая ссылочный тип с типом метода. Если типы несовместимы, проверка завершается с ошибкой, и программа сработает.

Это работает так же, как и для параметров типового типа, как и для не-генерических типов. Чтобы достичь этого, VES ограничивает методы, которые могут быть вызваны ссылкой, тип которой является неограниченным параметром общего типа. Единственными допустимыми методами являются те, которые определены в System.Object, потому что все объекты являются экземплярами этого типа.

Для типа с ограниченным параметром ссылки этого типа могут принимать вызовы методов, определенных типами ограничений. Например, если вы пишете метод, в котором у вас есть ограниченный тип T, который должен быть получен из ICollection, вы можете вызвать getter ICollection.Count по ссылке типа T. VES знает, что безопасно вызывать этот getter, потому что он гарантирует, что любая ссылка, хранящаяся в этой позиции в стеке, будет экземпляром некоторого типа, реализующего интерфейс ICollection. Независимо от того, каков фактический тип объекта, JIT-компилятор может поэтому использовать один и тот же собственный код.

Рассмотрим также поля, которые зависят от параметра типового типа. В случае List<T> существует массив типа T[], который содержит элементы в списке. Помните, что фактический массив в памяти будет массивом ссылок объектов O. Нативный код для построения этого массива или для чтения или записи его элементов выглядит одинаково независимо от того, является ли массив членом List<string> или List<FileInfo>.

Итак, в рамках неограниченного типа общего типа, такого как List<T>, ссылки T так же хороши, как System.Object ссылки. Преимущество дженериков состоит в том, что VES заменяет аргумент типа для параметра типа в области вызова. Другими словами, несмотря на то, что List<string> и List<FileInfo> обрабатывают свои элементы одинаково внутри, вызывающие абоненты видят, что метод Find возвращает a string, а другой - a FileInfo.

Наконец, поскольку все это достигается метаданными в IL, и поскольку VES использует метаданные при загрузке и JIT-компилирует типы, информация может быть извлечена во время выполнения через отражение.

Ответ 2

Вы спросили, как касты (включая is и as) могут работать с переменными общего типа. Так как все объекты хранят метаданные о своем собственном типе, все приведения работают так же, как если бы вы использовали тип переменной object. Объект опрошен о его типе и выполняется время выполнения.

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

Как работает typeof(T)? Это значение передается как скрытый параметр для общих методов. Это также то, как someObj as T может работать. Я уверен, что он компилируется как вызов помощнику во время выполнения (например, RuntimeCastHelper(someObj, typeof(T))).

Ответ 3

Среда выполнения clr компилирует каждый метод отдельно как раз вовремя, когда он выполняется первым. Вы можете увидеть это, если вы используете тип где-то в методе с несколькими строками и dll, тип которого определен в отсутствует. Установите точку останова в первой строке метода. При вызове метода генерируется исключение загрузки типа. Отбойник не попадает на точку останова. Теперь отделим метод в трех методах. Средний должен содержать строки с отсутствующим типом. Теперь вы можете войти в метод с отладчиком, а также в первый из новых методов, но при вызове второго, это исключение. Это связано с тем, что метод компилируется при первом вызове, и только тогда компилятор/компоновщик натыкается на отсутствующий тип.

Чтобы ответить на ваш вопрос: как указывали другие, дженерики поддерживаются в ИЛ. Во время выполнения, когда вы создаете Список в первый раз, код конструктора компилируется (с подстановкой int для параметра type). Если вы затем создаете список в первый раз, код снова скомпилируется со строкой в ​​качестве параметра типа. Вы можете видеть это, как будто конкретные классы с конкретными типами генерируются во время выполнения на лету.

Ответ 4

how does the compiler provides this type information to the generic methods?

TL;DR Он предоставляет информацию о типе, эффективно дублируя метод для каждого уникального типа, с которым он используется.

Теперь для тех из вас, кто хочет читать больше...;) Ответ на самом деле довольно простой, как только вы получите небольшой пример, чтобы пойти с ним.

Начнем с этого:

public static class NonGenericStaticClass
{
    public static string GenericMethod<T>(T value)
    {
        if(value is Foo)
        {
            return "Foo";
        }
        else if(value is Bar)
        {
            return "Bar";
        }
        else
        {
            return string.Format("It a {0}!", typeof(T).Name);
        }
    }
}

// ...

static void Main()
{
    // Prints "It a Int32!"
    Console.WriteLine(NonGenericStaticClass.GenericMethod<int>(100));

    // Prints "Foo"
    Console.WriteLine(NonGenericStaticClass.GenericMethod<Foo>(new Foo()))

    // Prints "It a Int32!"
    Console.WriteLine(NonGenericStaticClass.GenericMethod<int>(20));
}

Теперь, как уже отмечали другие люди, IL поддерживает generics изначально, поэтому компилятор С# на самом деле не очень много работает с этим примером. Однако, когда компилятор Just-In-Time приходит, чтобы превратить IL в машинный код, он должен преобразовать общий код во что-то, что не является общим. Для этого компилятор .Net Just-In-Time эффективно дублирует метод для каждого из разных типов, которые используются с ним.

Если полученный код был на С#, он, вероятно, выглядел бы примерно так:

public static class NonGenericStaticClass
{
    // The JIT Compiler might rename these methods after their
    // representative types to avoid any weird overload issues, but I'm not sure
    public static string GenericMethod(Int32 value)
    {
        // Note that the JIT Compiler might optimize much of this away
        // since the first 2 "if" statements are always going to be false
        if(value is Foo)
        {
            return "Foo";
        }
        else if(value is Bar)
        {
            return "Bar";
        }
        else
        {
            return string.Format("It a {0}!", typeof(Int32).Name);
        }
    }

    public static string GenericMethod(Foo value)
    {
        if(value is Foo)
        {
            return "Foo";
        }
        else if(value is Bar)
        {
            return "Bar";
        }
        else
        {
            return string.Format("It a {0}!", typeof(Foo).Name);
        }
    }
}

// ...

static void Main()
{
    // Notice how we don't need to specify the type parameters any more.
    // (of course you could've used generic inference, but that beside the point),
    // That is because they are essentially, but not necessarily, overloads of each other

    // Prints "It a Int32!"
    Console.WriteLine(NonGenericStaticClass.GenericMethod(100));

    // Prints "Foo"
    Console.WriteLine(NonGenericStaticClass.GenericMethod(new Foo()))

    // Prints "It a Int32!"
    Console.WriteLine(NonGenericStaticClass.GenericMethod(20));
}

Как только вы создали не общие методы, вы точно знаете, с каким типом вы сталкиваетесь, используя прекрасное использование статическая отправка.

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

Для некоторого контраста компилятор Java "обманывает" дженерики. Вместо того, чтобы генерировать новые типы и методы, такие как .Net, Java вставляет отливки, где вы ожидаете, что значение будет определенного типа. Таким образом, наш typeof(T) был бы невозможен в мире Java, вместо этого нам пришлось бы использовать метод getClass().