foreach за кулисами для string[] vs List‹string›

Почему в CIL компилятор преобразует цикл foreach в цикл for, когда используется массив, но использует шаблон итератора, когда используется List<T>?

Если и System.Array, и System.Collections.Generic.List<T> реализуют IEnumerable, не должны ли они оба использовать шаблон итератора за кулисами?

Вот пример:

Консольное приложение 1:

С#:

class Program
{
    static void Main(string[] args)
    {
        var enumerable = new List<string> { "a", "b" };

        foreach (string item in enumerable)
        {
            string x = item;
        }
    }
}

CIL:

.method private hidebysig static 
    void Main (
        string[] args
    ) cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 80 (0x50)
    .maxstack 3
    .entrypoint
    .locals init (
        [0] class [mscorlib]System.Collections.Generic.List`1<string> enumerable,
        [1] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<string>,
        [2] string item,
        [3] string x
    )

    IL_0000: nop
    IL_0001: newobj instance void class [mscorlib]System.Collections.Generic.List`1<string>::.ctor()
    IL_0006: dup
    IL_0007: ldstr "a"
    IL_000c: callvirt instance void class [mscorlib]System.Collections.Generic.List`1<string>::Add(!0)
    IL_0011: nop
    IL_0012: dup
    IL_0013: ldstr "b"
    IL_0018: callvirt instance void class [mscorlib]System.Collections.Generic.List`1<string>::Add(!0)
    IL_001d: nop
    IL_001e: stloc.0
    IL_001f: nop
    IL_0020: ldloc.0
    IL_0021: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> class [mscorlib]System.Collections.Generic.List`1<string>::GetEnumerator()
    IL_0026: stloc.1
    .try
    {
        IL_0027: br.s IL_0035
        // loop start (head: IL_0035)
            IL_0029: ldloca.s 1
            IL_002b: call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<string>::get_Current()
            IL_0030: stloc.2
            IL_0031: nop
            IL_0032: ldloc.2
            IL_0033: stloc.3
            IL_0034: nop

            IL_0035: ldloca.s 1
            IL_0037: call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<string>::MoveNext()
            IL_003c: brtrue.s IL_0029
        // end loop

        IL_003e: leave.s IL_004f
    } // end .try
    finally
    {
        IL_0040: ldloca.s 1
        IL_0042: constrained. valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<string>
        IL_0048: callvirt instance void [mscorlib]System.IDisposable::Dispose()
        IL_004d: nop
        IL_004e: endfinally
    } // end handler

    IL_004f: ret
} // end of method Program::Main

Консольное приложение 2:

С#:

class Program
{
    static void Main(string[] args)
    {
        var enumerable = new string[] { "a", "b" };

        foreach (string item in enumerable)
        {
            string x = item;
        }
    }
}

CIL:

.method private hidebysig static 
    void Main (
        string[] args
    ) cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 51 (0x33)
    .maxstack 4
    .entrypoint
    .locals init (
        [0] string[] enumerable,
        [1] string[],
        [2] int32,
        [3] string item,
        [4] string x
    )

    IL_0000: nop
    IL_0001: ldc.i4.2
    IL_0002: newarr [mscorlib]System.String
    IL_0007: dup
    IL_0008: ldc.i4.0
    IL_0009: ldstr "a"
    IL_000e: stelem.ref
    IL_000f: dup
    IL_0010: ldc.i4.1
    IL_0011: ldstr "b"
    IL_0016: stelem.ref
    IL_0017: stloc.0
    IL_0018: nop
    IL_0019: ldloc.0
    IL_001a: stloc.1
    IL_001b: ldc.i4.0
    IL_001c: stloc.2
    IL_001d: br.s IL_002c
    // loop start (head: IL_002c)
        IL_001f: ldloc.1
        IL_0020: ldloc.2
        IL_0021: ldelem.ref
        IL_0022: stloc.3
        IL_0023: nop
        IL_0024: ldloc.3
        IL_0025: stloc.s x
        IL_0027: nop
        IL_0028: ldloc.2
        IL_0029: ldc.i4.1
        IL_002a: add
        IL_002b: stloc.2

        IL_002c: ldloc.2
        IL_002d: ldloc.1
        IL_002e: ldlen
        IL_002f: conv.i4
        IL_0030: blt.s IL_001f
    // end loop

    IL_0032: ret
} // end of method Program::Main

person David Klempfner    schedule 15.09.2019    source источник
comment
Вы используете dnSpy? Это дает вам всплывающие подсказки, которые объясняют каждый вызов IL. Имеет смысл, что массиву не нужен итератор, так как это непрерывное выделение памяти. Это очевидная оптимизация, чтобы сделать это таким образом.   -  person Jeremy Thompson    schedule 15.09.2019
comment
@JeremyThompson Но список - это просто массив за кулисами.   -  person David Klempfner    schedule 15.09.2019
comment
Если компилятор знает, что IEnumerable является массивом, имеет смысл оптимизировать foreach в for; намного проще (/быстрее) перебирать массив по индексу, чем с помощью любого другого алгоритма MoveNext/Current, который я могу придумать.   -  person Flydog57    schedule 15.09.2019
comment
Разница в том, что с массивами не выделяется объект для управления итерацией, а проверка границ удаляется. При использовании списков переменная управления итерацией выделяется в стеке, и выполняется проверка границ. Итак, понятно, почему разработчики языка использовали цикл For (изменение ForEach в выводе IL) с массивами при итерации.   -  person Jeremy Thompson    schedule 15.09.2019
comment
@JeremyThompson Теперь я понимаю. Эрик Липперт очень хорошо выразил это здесь: за кулисами цикла foreach">stackoverflow.com/questions/7350495/ Это не обязательно должен быть сгенерированный код; все, что требуется, — это сгенерировать код, дающий тот же результат. Например, если вы выполняете foreach для массива или строки, мы просто генерируем цикл for   -  person David Klempfner    schedule 15.09.2019
comment
@MickyD, но System.Array реализует IEnumerable. На этой странице говорится, что Array использует GetEnumerator(): docs.microsoft.com/en-us/dotnet/api/   -  person David Klempfner    schedule 15.09.2019
comment
@backwards_dave: на этой странице написано, что Array реализует GetEnumerator. Также говорится, что рекомендуется использовать foreach вместо непосредственного управления перечислителем. Конечно, вы можете вызвать GetEnumerator, а затем MoveNext/Current для результата. Но это не мешает компилятору делать что-то еще эквивалентное под прикрытием   -  person Flydog57    schedule 15.09.2019


Ответы (1)


Разница в том, что с массивами не выделяется объект для управления итерацией, а проверка границ удаляется. При использовании списков переменная управления итерацией выделяется в стеке, и выполняется проверка границ. Итак, понятно, почему разработчики языка использовали цикл For (изменение ForEach в выводе IL) с массивами при итерации.

Поскольку массив не поддерживает добавление/удаление элементов, подразумевается фиксированная длина. Таким образом, без проверки границ это оптимизация для доступа к элементам массива по индексу, а не по итератору (реализация IEnumerable).

person Jeremy Thompson    schedule 15.09.2019