[Перевод - recovery mode ] C#: Внутреннее строение инициализаторов массивов
Наверняка почти каждому, кто имел дело с 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» майку (у кого имеется) и погрузимся в недра реализации.В конечном итоге, первая конструкция превратится компилятором в такую вот загогулину:
Что за диво в моём саду? Что это за
.method private hidebysig static void Main () cil managed
{
.entrypoint
// Code size 20 (0×14)
.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 '
На деле, выделенная линия являет собой статично поле в приватном и, очевидно, сгенерированном компилятором, классе с очевидно непроизносимым названием. Пара моментов, на которые следует обратить внимание. По-первых, этот класс имеет вложенный класс, названный __StaticArrayInitTypeSize=12. Он являет собой массив фактическим размером в 12 байт (по 4 байта на каждый элемент System.Int32, размер каждого равен 4 байта, итого 12). Во-вторых, следует заметить что тип наследует System.ValueType (я всерьёз надеюсь на то, что читатели знакомы с судьбой экземпляров значимых типов после их создания в стеке, так что не будем на этом застрять внимания — прим. автора.). Но каким образом тип получает те самые 12 байт? Очевидно что просто подсунуть имя недостаточно для того, чтобы clr выделила необходимое количество памяти, так что если вы посмотрите на реализацию через ILDASM вы увидите вот что:
.class private auto ansi '
.field static assembly valuetype '
.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 '
.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 (0×13) .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 команды на каждый элемент, что увеличивало бы обьём кода построения массива линейно пропорционально размеру массива, вместо этого обьём кода константен.