Разница между экземпляром call и экземпляром newobj в IL

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

    private static void HowNullableWorks()
    {
        int test = 3;
        int? implicitConversion = test;
        Nullable<int> test2 = new Nullable<int>(3);

        MethodThatTakesNullableInt(null);
        MethodThatTakesNullableInt(39);
    }

И я был удивлен, увидев, что переменные implicitConversion / test2 инициализируются с помощью:

call       instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)

инструкция, тогда как при вызове MethodThatTakesNullableInt я вижу:

IL_0017:  initobj    valuetype [mscorlib]System.Nullable`1<int32>

а также

IL_0026:  newobj     instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)

что я понимаю. Я думал, что увижу инструкцию newobj для implicitConversion / test2.

Это полный IL-код:

.method private hidebysig static void  HowNullableWorks() cil managed
{
  // Code size       50 (0x32)
  .maxstack  2
  .locals init ([0] int32 test,
           [1] valuetype [mscorlib]System.Nullable`1<int32> implicitConversion,
           [2] valuetype [mscorlib]System.Nullable`1<int32> test2,
           [3] valuetype [mscorlib]System.Nullable`1<int32> CS$0$0000)
  IL_0000:  nop
  IL_0001:  ldc.i4.3
  IL_0002:  stloc.0
  IL_0003:  ldloca.s   implicitConversion
  IL_0005:  ldloc.0
  IL_0006:  call       instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)
  IL_000b:  nop
  IL_000c:  ldloca.s   test2
  IL_000e:  ldc.i4.3
  IL_000f:  call       instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)
  IL_0014:  nop
  IL_0015:  ldloca.s   CS$0$0000
  IL_0017:  initobj    valuetype [mscorlib]System.Nullable`1<int32>
  IL_001d:  ldloc.3
  IL_001e:  call       void csharp.in.depth._2nd.Program::MethodThatTakesNullableInt(valuetype [mscorlib]System.Nullable`1<int32>)
  IL_0023:  nop
  IL_0024:  ldc.i4.s   39
  IL_0026:  newobj     instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)
  IL_002b:  call       void csharp.in.depth._2nd.Program::MethodThatTakesNullableInt(valuetype [mscorlib]System.Nullable`1<int32>)
  IL_0030:  nop
  IL_0031:  ret
} // end of method Program::HowNullableWorks

c# il
person dragonfly    schedule 15.08.2012    source источник


Ответы (1)


Во-первых, похоже, что вы скомпилировали в режиме отладки (на основе nops) — возможно, вы увидите другой код, если вы скомпилируете в режиме выпуска.

В разделе I.12.1.6.2.1 спецификации ECMA CLR (Инициализация экземпляров типов значений) говорится:

Существует три варианта инициализации домашней страницы экземпляра типа значения. Вы можете обнулить его, загрузив адрес дома (см. Таблицу I.8: Адрес и тип местоположения дома) и используя инструкцию initobj (для локальных переменных это также достигается установкой бита localsinit в заголовке метода). Вы можете вызвать пользовательский конструктор, загрузив адрес дома (см. Таблицу I.8: Адрес и тип местоположения дома), а затем напрямую вызвав конструктор. Или вы можете скопировать существующий экземпляр в дом, как описано в §I.12.1.6.2.2.

Первые три использования типов, допускающих значение NULL, в вашем коде приводят к нулевым значениям, хранящимся в локальных переменных, поэтому этот комментарий актуален (локальные переменные — это один из типов home для значений): первые две — это локальные переменные implicitConversion и test. который вы объявили, а третий — созданный компилятором временный файл с именем CS$0$0000. Как указано в спецификации ECMA, эти локальные переменные можно инициализировать с помощью initobj (что эквивалентно конструктору без аргументов по умолчанию для структуры и в данном случае используется для CS$0$0000) или путем загрузки адреса локальной переменной и вызова конструктора (используется для двух других местных жителей).

Однако для окончательного обнуляемого экземпляра (созданный неявным преобразованием из 39) результат не сохраняется в локальном — он генерируется в стеке, поэтому правила инициализации дома здесь не применяются. Вместо этого компилятор просто использует newobj для создания значения в стеке (как и для любого значения или ссылочного типа).

Вам может быть интересно, почему компилятор сгенерировал локальную переменную для вызова MethodThatTakesNullableInt(null), но не для MethodThatTakesNullableInt(39). Я подозреваю, что ответ заключается в том, что компилятор всегда использует initobj для вызова конструктора по умолчанию (который затем требует локального или другого дома для значения), но использует newobj для вызова других конструкторов и сохранения результата в стеке, когда еще нет конструктора. соответствующий дом для значения.

Для получения дополнительной информации см. также этот комментарий из раздела III.4.21 (newobj) из спецификации:

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

person kvb    schedule 15.08.2012