C#: Внутреннее строение инициализаторов массивов

http://community.bartdesmet.net/blogs/bart/archive/2008/08/21/how-c-array-initializers-work.aspx
Наверняка почти каждому, кто имел дело с C#, известна подобная конструкция:

int[] ints = new int[3] { 1,2,3 };//А если уж вдруг и не была известна, то отныне и впредь уж точно

Вполне логично было-бы ожидать превращение этой конструкции в нечто подобное:

int[] ints = new int[3]; 
ints[0] = 1; 
ints[1] = 2; 
ints[2] = 3;

Увы и ах, на деле орех гораздо более морщинист, чем кажется с первого взгляда, и имеются некоторые тонкости, на которые будет указано позже. А до тех пор, наденем ношеную «IL freak» майку (у кого имеется) и погрузимся в недра реализации.

В конечном итоге, первая конструкция превратится компилятором в такую вот загогулину:



Что за диво в моём саду? Что это за <PrivateImplementationDetails>blablabla ерундовина? Прежде чем я расскажу что к чему, давайте бросим наш взгляд на Q::Main, где я указал значение в вершине стека перед каждой строкой кода:

.method private hidebysig static void  Main() cil managed 
{ 
  .entrypoint 
  // Code size       20 (0x14) 
  .maxstack  3 
  .locals init (int32[] V_0) 
  IL_0000:  nop 
  // {} 
  IL_0001:  ldc.i4.3 
  // {3} 
  IL_0002:  newarr     [mscorlib]System.Int32 
  // {&int[3]} 
  IL_0007:  dup 
  // {&int[3], &int[3]} 
  IL_0008:  ldtoken    field valuetype '<PrivateImplementationDetails>{8C802ECE-B24C-4A20-AE34-9303FE2DD066}'/'__StaticArrayInitTypeSize=12' '<PrivateImplementationDetails>{8C802ECE-B24C-4A20-AE34-9303FE2DD066}'::'$$method0x6000001-1' 
  // {&int[3], &int[3], #'$$method0x6000001-1'} 
  IL_000d:  call       void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array, 
                                                                                                      valuetype [mscorlib]System.RuntimeFieldHandle) 
  // {&int[3]} 
  IL_0012:  stloc.0 
  // {} 
  IL_0013:  ret 
} // end of method Q::Main

Давайте теперь проведём построчный анализ:
IL_0001 и IL_0002 — создаётся новый массив типа System.Int32 и размерностью 3.
На IL_0007 нам попадается первый сюрприз в виде дублированной ссылки на массив. Почему? Предположим, что на IL_0008 и IL_0009 происходит инициализация массива (совсем скоро к этому месту мы вернёмся). А теперь посмотрим на IL_0012, где значение в вершине стека — опять массив — присвоен локальной переменной с индексом 0, т.е. переменной ints. А что если мы присвоим значение переменной ints на IL_0007? А произойдёт вот что:

ldc.i4.3 
newarr     [mscorlib]System.Int32 
stloc.0 //внимание сюда
ldloc.0 //и сюда
ldtoken    field valuetype '<PrivateImplementationDetails>{8C802ECE-B24C-4A20-AE34-9303FE2DD066}'/'__StaticArrayInitTypeSize=12' '<PrivateImplementationDetails>{8C802ECE-B24C-4A20-AE34-9303FE2DD066}'::'$$method0x6000001-1' 
call       void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array, 
                                                                                          valuetype [mscorlib]System.RuntimeFieldHandle)

Присвоение более не будет атомарным: с этого момента, внешний наблюдатель заметит массив в неинициализированном состоянии, без элементов. Именно этим и занимаются строки IL_0008 и IL_0009. Т.о. приведённый в самом начале код не эквивалентен конструкции:

nt[] ints = new int[3]; 
ints[0] = 1; 
ints[1] = 2; 
ints[2] = 3;

А скорее представляет из себя нечто вроде этого:

int[] t = new int[3]; 
t[0] = 1; 
t[1] = 2; 
t[2] = 3; 
int[] ints = t;

Хотя реализация избегает создания двух локальных переменных. Это двигает нас к двум заумным строчкам кода:

 IL_0008:  ldtoken    field valuetype '<PrivateImplementationDetails>{8C802ECE-B24C-4A20-AE34-9303FE2DD066}'/'__StaticArrayInitTypeSize=12' '<PrivateImplementationDetails>{8C802ECE-B24C-4A20-AE34-9303FE2DD066}'::'$$method0x6000001-1' 
  IL_000d:  call       void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array,  valuetype [mscorlib]System.RuntimeFieldHandle)

Но ничего сложного и/или страшного в этом нет. По сути, мы наблюдаем вызов RuntimeHelpers.InitializeArray, который заполняет поле, токен которого помещён в стек на IL_0008, массивом, ссылка на который находится в вершине стека после выполнения IL_0007. Значение токена соотвествует картинке ниже:



На деле, выделенная линия являет собой статично поле в приватном и, очевидно, сгенерированном компилятором, классе с очевидно непроизносимым названием. Пара моментов, на которые следует обратить внимание. Во-первых, этот класс имеет вложенный класс, названный __StaticArrayInitTypeSize=12. Он являет собой массив фактическим размером в 12 байт (по 4 байта на каждый элемент System.Int32, размер каждого равен 4 байта, итого 12). Во-вторых, следует заметить что тип наследует System.ValueType (я всерьёз надеюсь на то, что читатели знакомы с судьбой экземпляров значимых типов после их создания в стеке, так что не будем на этом застрять внимания — прим. автора.). Но каким образом тип получает те самые 12 байт? Очевидно что просто подсунуть имя недостаточно для того, чтобы clr выделила необходимое количество памяти, так что если вы посмотрите на реализацию через ILDASM вы увидите вот что:

.class private auto ansi '<PrivateImplementationDetails>{8C802ECE-B24C-4A20-AE34-9303FE2DD066}' 
       extends [mscorlib]System.Object 
{ 
  .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) 
  .class explicit ansi sealed nested private '__StaticArrayInitTypeSize=12' 
         extends [mscorlib]System.ValueType 
  { 
    .pack 1 
    .size 12 //внимание сюда и тут
  } // end of class '__StaticArrayInitTypeSize=12'

  .field static assembly valuetype '<PrivateImplementationDetails>{8C802ECE-B24C-4A20-AE34-9303FE2DD066}'/'__StaticArrayInitTypeSize=12' '$$method0x6000001-1' at I_00002050 
} // end of class '<PrivateImplementationDetails>{8C802ECE-B24C-4A20-AE34-9303FE2DD066}'

Директива .size как-бы говорит и нам, и clr о том, что необходимо выделить блок памяти в 12 байт в момент создания экземпляра этого типа. Если вам любопытно о роли директивы .pack, то суть проста: эта директива указывает выравнивание по указанной степени двойки (поддерживаются только значения от 2 до 128 (при значении 1 выравнивание, очевидно, отсутствует — прим. перев)). Необходимо для COM-совместимости. Вернёмся к полю:

.field static assembly valuetype '<PrivateImplementationDetails>{8C802ECE-B24C-4A20-AE34-9303FE2DD066}'/'__StaticArrayInitTypeSize=12' '$$method0x6000001-1' at I_00002050


Тип довольно простой, несмотря на то, что имя довольно длинное из-за вложенности типов. В нашем случае, '$$method0x6000001-1' это имя поля. Но самое интересное начинается после "at". Это т.н. data-label, который, в свою очередь, является куском данных где-то в PE-файле на данном смещении. Непосредственно в ILADSM вы увидите нечто подобное:

.data cil I_00002050 = bytearray ( 
                 01 00 00 00 02 00 00 00 03 00 00 00)

Это и есть обьявление data label, которое является, как уже видно, последовательностью байт конечного массива в little-endian. Теперь мы должны понимать как работает InitailizeArray:

call void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array, valuetype [mscorlib]System.RuntimeFieldHandle)

Передаётся экземпляр массива (мы уже создали его командами IL_0001,IL_0002) и указатель на поле, указанное после ключевого слова "at", в которое завёрнуты данные массива. Т.о. среда исполнения способна посчитать необходимое количество байт для чтения по заданному адресу, конструируя таким образом массив. В свою очередь, смысл значения I_00002050 не являет собой никакой загадки — это преобыкновеннейший RVA. Вы можете в этом убедиться, используя dumpbin:

Но есть не менее занятная деталь: компилятор переиспользует тип __StaticArrayInitTypeSize когда массивы занимают одинаковое количество места в памяти. Т.о. листинг:

int[]  ints  = { 1, 2, 3, 4, 5, 6, 7, 8 }; 
long[] longs = { 1, 2, 3, 4 }; 
byte[] bytes = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32 };

Заставляет компилятор использовать один и тот-же тип, ибо все массивы в памяти занимают по 32 байта:

.field static assembly valuetype '<PrivateImplementationDetails>{AA6C9D77-5FAD-47E0-8B55-1D8739074F1F}'/'__StaticArrayInitTypeSize=32' '$$method0x6000001-1' at I_00002050 
.field static assembly valuetype '<PrivateImplementationDetails>{AA6C9D77-5FAD-47E0-8B55-1D8739074F1F}'/'__StaticArrayInitTypeSize=32' '$$method0x6000001-2' at I_00002070 
.field static assembly valuetype '<PrivateImplementationDetails>{AA6C9D77-5FAD-47E0-8B55-1D8739074F1F}'/'__StaticArrayInitTypeSize=32' '$$method0x6000001-3' at I_00002090 

.data cil I_00002050 = bytearray ( 
                 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 
                 05 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00) 
.data cil I_00002070 = bytearray ( 
                 01 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 
                 03 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00) 
.data cil I_00002090 = bytearray ( 
                 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 
                 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20)

Так-же, для массивов размером в 1 и 2 элемента, будет генерировать такой IL код:
.method private hidebysig static void  Main() cil managed 
{ 
  .entrypoint 
  // Code size       19 (0x13) 
  .maxstack  3 
  .locals init (int32[] V_0, 
           int32[] V_1) 
  IL_0000:  nop 
  IL_0001:  ldc.i4.2 
  IL_0002:  newarr     [mscorlib]System.Int32 
  IL_0007:  stloc.1 

  // 
  // V_1[0] = 1 
  // 
  IL_0008:  ldloc.1 
  IL_0009:  ldc.i4.0 
  IL_000a:  ldc.i4.1 
  IL_000b:  stelem.i4 

  // 
  // V_1[1] = 2 
  // 
  IL_000c:  ldloc.1 
  IL_000d:  ldc.i4.1 
  IL_000e:  ldc.i4.2 
  IL_000f:  stelem.i4 

  // 
  // V_0 = V_1 
  // 
  IL_0010:  ldloc.1 
  IL_0011:  stloc.0 

  IL_0012:  ret 
} // end of method Q::Main

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

p.s. В статье приведено поведение компиляторов C# 2.0 и 3.0 версий от Microsoft. Поведение кода, сгенерированного компиляторами других версий или компиляторов от сторонних разработчиков (например, Mono), может отличаться от приведённого в статье.
Поделиться публикацией
Комментарии 31
    +3
    Очевидно, никто.
      –8
      Это всё, конечно, замечательно, но какая практическая польза от этой информации?
        +6
        1. Более глубокое понимание того, что происходит. «Вы должны понимать как минимум на один уровень абстракции ниже того уровня, на котором вы кодируете» (с) Ли Кэмпбел.
        2. Понимание таких вещей очень полезно, если вы занимаетесь кодогенерацией, делаете IL-инъекции, активно используете Emit и т.п. Ну и, разумеется, это пригодится тем, кто ввиду специфики своего проекта активно работает с IL.
        3. А ещё есть такая штука, как критичный по производительности код. Если вы хотите очень сильно разогнать ваше C#-приложение, то вам очень не помешает понимать какие IL-инструкции стоят за каждой C#-строчкой (а лучше бы ещё и иметь представление про итоговые машинные коды).

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

          Ну или просто для удовлетворения обыкновенного животного любопытства.
            0
            Это само собой =) Просто я отвечал на вопрос про практическую пользу.
            +2
            Бьюсь об заклад, что при использовании emit вы не будете копировать это поведение, а тупо в лоб создадите массив (статический, если используется более одного раза).

            Обсуждение производительности всего описанного вами IL-а без опускания до машинных кодов именно что выглядит неполным.

            То есть в целом ни для «сеньоров», ни для «джуниоров» эта информация пользы не представляет. Чисто любопытство удовлетворить. О чём я, собственно, и говорю.
              –1
              До меня дошло. Эта информация ценна для джуниоров. Когда сеньор в следующий раз закричит, вытаращив глаза: «Ты что, опух при каждом вызове метода массив создавать?!» Джуниор сможет парировать: «А компилятор оптимизирует». :)
                0
                При таком способе инициализации массива дело, скорее всего, сводится к одному вызову memcpy (для первого массива). Отсюда и профит.
            +2
            Вот мне интересно это, хотя я на C# не пишу. Вот в Java эта проблема не решена и объявление довольно длинного массива (хоть в статическом поле, хоть в методе) вызовет ошибку компиляции Code too large. И это не какой-то запредельный размер массива, что-то типа:

            public class Test {
            static final double[] vals = {
              5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
              5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
              5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
            ... // 165 строчек пропущено
              5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
              5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,
            };
            }


            Каждый элемент массива отнимает 8 байт байткода из 65535 допустимых (первые 127 элементов меньше), то есть влезает около 8000 элементов массива. Автогенерированная lookup-таблица для какого-нибудь численного алгоритма вполне может уткнуться в этот предел. Естественно, я должен знать это, когда работаю над алгоритмами. Хорошо, что в C# с этим проще. Разработчики на C# тоже должны это знать.
              +2
              Я бы сказал, что полезно знать о наличии ограничений, а вот информация об их отсутствии менее полезна. Разве что в холиварах C# vs Java лишний аргумент. :)

              Кстати, вы меня заинтриговали. Накопипастил кода на 25К строк, программа скомпилилась в 10-метровый исполняемый файл. Работает. :) За компанию проверил, что будет, если в огромном массиве один элемент сделать неконстантным — таки тоже оптимизировалось.

              P.S. VS+R# под конец таки рухнули. Reflector тоже. Но скомпилировать и декомпилировать пару раз смогли.
                0
                На самом деле мне непонятно, почему нельзя было предусмотреть в class-файлах Java и в исполняемых файлах .Net специальную секцию типа «бинарный блок», в которую пихать в сыром виде содержимое массива, и специальную инструкцию байткода типа «скопируй кусок из бинарного блока в массив, который сейчас в верхушке стека». В шарпе что-то подобное сделали, но извратным путём с какими-то вложенными классами и вспомогательными методами. Может, конечно, это не настолько часто встречается, чтобы заморачиваться. Если данных много, разумно сделать дополнительный файл ресурсов в пакете и вычитать из него.
                  0
                  Э… Чтение из ресурсов — одна строчка кода (в шарпе, по крайней мере). Зачем какие-то новые абстракции?
            +15
            По старой доброй традиции самое непонятное начинается после слова «очевидно»
              +2
              -Сколько мы выводили эту формулу?
              -Полтора месяца
              -Пиши: «Легко показать, что...»
                0
                Ну шутки-шутками, может я, конечно, БЕЗНОГNМ, но вот идею фокуса с двумя переменными и как там вообще может образоваться выигрыш я не понял.
                  0
                  Меня это тоже удивило. Про выигрыш ничего не было сказано, да и его и не будет, т.к. простой вариант вместо dup будет делать stloc.0 и ldloc.0. Однако автор говорит про атомарность варианта с dup, но что он под этим подразумевает?
              +3
              Я бы на вашем месте сделал акцент на том, что всё это особенности текущей версии компилятора C# от Microsoft. Насколько я знаю, спецификации C# нам ничего подобного не говорят. Пример: под Mono 2.4 приведённый код превращался в точности в
              int[] ints = new int[3]; 
              ints[0] = 1; 
              ints[1] = 2; 
              ints[2] = 3;
              

              Исследование очень интересное и полезное, но не стоит забывать, что Microsoft нам не гарантирует, что в будущих версиях компилятора поведение не поменяется. Поэтому разумно будет указать версии компилятора C#, для которых выводы справедливы. Roslyn выдаёт аналогичные результаты, но кто его знает, что будет потом.

              Барт де Смет писал оригинальный пост в далёком 2008, ещё до выхода Mono 2.0. В то время люди вообще не задумывались о том, что могут быть альтернативы реализации от Microsoft. А вот в наши дни надо бы указывать (хотя бы в примечаниях переводчика), что это особенности конкретных компиляторов.
                +1
                Я думал о том, чтобы добавить строчку относительно mono, но возможное несоответствие поведения у mono и ms компиляторов должно быть уже привычной вещью. Насчёт указания версии компилятора — чёрт его знает, оригиналу статьи скоро вот уже 5 лет и пока ничего не изменилось.
                p.s. поведение компилятора mono, используемого в unity3d, пока-что повторяет поведение, указанное в статье.
                  +2
                  несоответствие поведения у mono и ms компиляторов должно быть уже привычной вещью

                  Ну, это не значит, что будет лишним напомнить об этом людям.

                  оригиналу статьи скоро вот уже 5 лет и пока ничего не изменилось

                  Сейчас .NET-отрасль развивается очень активно. Я не удивлюсь, если после выпуска Roslyn MS начнут улучшать IL-генерацию и чего-нибудь поменяют. В этом случае будущий читатель вашей статьи скажет спасибо за то, что вы указали версии компиляторов, для которых всё это работает.

                  поведение компилятора mono, используемого в unity3d, пока-что повторяет поведение, указанное в статье

                  Да, Xamarin пофиксили поведение в Mono 2.10. Но это не отменяет того факта, что есть ещё индивидуумы, которые сидят под Mono 2.4.
                0
                Спасибо. Весьма познавательно.
                  +1
                  Еще было бы интересно посмотреть сравнение с точки зрения производительности, какой именно выигрыш дает эта оптимизация.
                    0
                    У меня есть в планах на будущее провести аккуратный бенчмарк. Как сделаю — скину ссылку на результаты.
                    0
                    > Вполне логично было-бы ожидать превращение этой конструкции в нечто подобное:

                    А мне вот кажется что нелогично. У нас конструктор и инициализацией значений. А если вспомнить во что превращается
                    var s = new SomeClass() { Id = 1 };
                    то как раз логично ожидать как раз вариант с промежуточной переменной.
                      0
                      Вынужден вас разочаровать: инициализатор обьекта компилируется самым наивным образом.
                      class c1
                              {
                                  public int _1;
                                  public int _2;
                                  public int _3;
                                  public int _4;
                              }
                      
                              void foo()
                              {
                                  new c1() { _1 = 3, _3 = 2, _2 = 1 };
                              }
                      


                      .method private hidebysig instance void  foo() cil managed
                      {
                        // Code size       31 (0x1f)
                        .maxstack  2
                        .locals init ([0] class ReflectionEmitArrayInitializerExample.Program/c1 '<>g__initLocal0')
                        IL_0000:  nop
                        IL_0001:  newobj     instance void ReflectionEmitArrayInitializerExample.Program/c1::.ctor()
                        IL_0006:  stloc.0
                        IL_0007:  ldloc.0
                        IL_0008:  ldc.i4.3
                        IL_0009:  stfld      int32 ReflectionEmitArrayInitializerExample.Program/c1::_1
                        IL_000e:  ldloc.0
                        IL_000f:  ldc.i4.2
                        IL_0010:  stfld      int32 ReflectionEmitArrayInitializerExample.Program/c1::_3
                        IL_0015:  ldloc.0
                        IL_0016:  ldc.i4.1
                        IL_0017:  stfld      int32 ReflectionEmitArrayInitializerExample.Program/c1::_2
                        IL_001c:  ldloc.0
                        IL_001d:  pop
                        IL_001e:  ret
                      } // end of method Program::foo
                      
                        0
                        А какая версия Studio и .NET. Сейчас собрал такой же код под VS13+.NET4.5 — вижу 2 переменные (насколько я понимаю в IL)
                          0
                          2010 студия, версия языка — 3.0
                          p.s. можно посмотреть на IL-код?
                      0
                          .method public hidebysig instance void Foo () cil managed 
                          {
                              .locals init (
                                  [0] class ClassLibrary1.Class1 c,
                                  [1] class ClassLibrary1.Class1 '<>g__initLocal0'
                              )
                      
                              IL_0000: nop
                              IL_0001: newobj instance void ClassLibrary1.Class1::.ctor()
                              IL_0006: stloc.1
                              IL_0007: ldloc.1
                              IL_0008: ldc.i4.3
                              IL_0009: stfld int32 ClassLibrary1.Class1::_1
                              IL_000e: ldloc.1
                              IL_000f: ldc.i4.1
                              IL_0010: stfld int32 ClassLibrary1.Class1::_2
                              IL_0015: ldloc.1
                              IL_0016: ldc.i4.2
                              IL_0017: stfld int32 ClassLibrary1.Class1::_3
                              IL_001c: ldloc.1
                              IL_001d: stloc.0
                              IL_001e: ret
                          }
                      


                      Оригинал:
                              public void Foo()
                              {
                                  Class1 class1 = new Class1()
                                  {
                                      _1 = 3,
                                      _2 = 1,
                                      _3 = 2
                                  };
                              }
                      
                        0
                        Код разный
                          0
                          Ну я о том же. В .NET 4.5 делается как раз через промежуточную переменную, что в принципе логично.
                            0
                            void foo()
                                    {
                                        new c1() { _1 = 3, _3 = 2, _2 = 1 };
                                    }
                            

                            public void Foo()
                                    {
                                        Class1 class1 = new Class1()
                                        {
                                            _1 = 3,
                                            _2 = 1,
                                            _3 = 2
                                        };
                                    }
                            
                              0
                              А, не заметил это с ходу. :) Ну тогда мое исходное утверждение получается верно — при использовании инициализаторов сборка класса происходит в промежуточной переменной. а уже потом присвоение в заданную. Соответственно и насчет инициализации массива тоже самое (см. мой первый комментарий). Не ясно тогда о чем спор :)

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое