Как, когда и где генерируются общие методы?

Этот вопрос заставил меня задаться вопросом, где на самом деле возникает конкретное воплощение общего метода. Я пробовал Google, но не нашел подходящего поиска.

Если взять этот простой пример:

class Program
{
    public static T GetDefault<T>()
    {
        return default(T);
    }

    static void Main(string[] args)
    {
        int i = GetDefault<int>();
        double d = GetDefault<double>();
        string s = GetDefault<string>();
    }
}

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

class Program
{
    static void Main(string[] args)
    {
        int i = GetDefaultSystemInt32();
        double d = GetDefaultSystemFloat64();
        string s = GetDefaultSystemString();
    }

    static int GetDefaultSystemInt32()
    {
        int i = 0;
        return i;
    }
    static double GetDefaultSystemFloat64()
    {
        double d = 0.0;
        return d;
    }
    static string GetDefaultSystemString()
    {
        string s = null;
        return s;
    }
}

Глядя на IL для общей программы, он все еще выражается в терминах общих типов:

.method public hidebysig static !!T  GetDefault<T>() cil managed
{
  // Code size       15 (0xf)
  .maxstack  1
  .locals init ([0] !!T CS$1$0000,
           [1] !!T CS$0$0001)
  IL_0000:  nop
  IL_0001:  ldloca.s   CS$0$0001
  IL_0003:  initobj    !!T
  IL_0009:  ldloc.1
  IL_000a:  stloc.0
  IL_000b:  br.s       IL_000d
  IL_000d:  ldloc.0
  IL_000e:  ret
} // end of method Program::GetDefault

Итак, как и в какой момент он решил, что int, а затем двойной и затем строка должны быть выделены в стеке и возвращены вызывающему? Это операция JIT-процесса? Я смотрю на это совершенно неправильно?

Ответ 1

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

Фактический "конкретный" общий метод создается во время выполнения JIT и не существует в IL. В первый раз, когда общий тип используется с типом, JIT увидит, был ли он создан, а если нет, постройте соответствующий метод для этого родового типа.

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

Ответ 2

Фактический машинный код для общего метода создается, как всегда, при использовании метода. В этот момент дрожание сначала проверяет, был ли раньше подходящий кандидат. Это, как правило, случай, код для метода, конкретный тип выполнения T которого является ссылочным типом, должен генерироваться только один раз и подходит для любого возможного ссылочного типа T. Ограничения на T гарантируют, что этот машинный код всегда действителен, ранее проверенный компилятором С#.

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

Итак, да, в вашем случае вы получите три разных метода. Версия <string> может использоваться для любого ссылочного типа, но у вас нет других. И версии <int> и <double> соответствуют категории "T, которые являются типами значений".

В противном случае отличный пример, возвращаемые значения этих методов передаются обратно вызывающему по-разному. В случае джиттера x64 версия строки возвращает значение с регистром RAX, как и любое возвращаемое значение указателя, версия int возвращается с регистром EAX, двойная версия возвращается с регистром XMM0.