foreach 在 string[] 与 List<string> 的幕后

foreach behind the scenes for string[] vs List<string>

为什么在CIL中,编译器在使用数组时将foreach循环转换为for循环,而在List<T>时使用迭代器模式用过?

如果 System.ArraySystem.Collections.Generic.List<T> 都实现了 IEnumerable,他们不应该都在幕后使用迭代器模式吗?

这是一个例子:

控制台应用程序 1:

C#:

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:

C#:

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

不同之处在于,对于数组,没有分配任何对象来管理迭代,并且删除了边界检查。对于列表,迭代管理变量是堆栈分配的,并且执行边界检查。所以很清楚为什么语言设计者在迭代时对数组使用 For 循环(在 IL 输出中更改 ForEach)。

由于数组不支持 Adding/Removing 项,因此存在隐含的固定长度。因此,如果没有边界检查,它是通过索引而不是迭代器(IEnumerable 实现)访问数组项的优化。