IL Discovery 系列三 《为什么在遍历List<T>对象时同时删除其中项会抛出异常》

对于如下简单的代码:

        static void ILDiscoveryListDelete()
        {
            List<int> list = new List<int>
            {
                1,2,3,4,
            };
            foreach (var item in list)
            {
                list.Remove(item);
            }
        }

在执行的时候会跑出如下异常:

deleteListItem

为什么会这样子呢,我们知道,foreach语句在执行的时候是通过被遍历对象(这里是List<T>对象)的枚举器来实现的。

我们首先关注List<T>对象枚举器的实现细节:

.method public hidebysig newslot virtual final instance bool MoveNext() cil managed
{
    .maxstack 3
    .locals init (
        [0] class System.Collections.Generic.List`1<!T> list)
    L_0000: ldarg.0 
    L_0001: ldfld class System.Collections.Generic.List`1<!0> System.Collections.Generic.List`1/Enumerator<!T>::list
    L_0006: stloc.0 
    L_0007: ldarg.0 
    L_0008: ldfld int32 System.Collections.Generic.List`1/Enumerator<!T>::version
    L_000d: ldloc.0 
    L_000e: ldfld int32 System.Collections.Generic.List`1<!T>::_version
    L_0013: bne.un.s L_004a
    L_0015: ldarg.0 
    L_0016: ldfld int32 System.Collections.Generic.List`1/Enumerator<!T>::index
    L_001b: ldloc.0 
    L_001c: ldfld int32 System.Collections.Generic.List`1<!T>::_size
    L_0021: bge.un.s L_004a
    L_0023: ldarg.0 
    L_0024: ldloc.0 
    L_0025: ldfld !0[] System.Collections.Generic.List`1<!T>::_items
    L_002a: ldarg.0 
    L_002b: ldfld int32 System.Collections.Generic.List`1/Enumerator<!T>::index
    L_0030: ldelem.any !T
    L_0035: stfld !0 System.Collections.Generic.List`1/Enumerator<!T>::current
    L_003a: ldarg.0 
    L_003b: dup 
    L_003c: ldfld int32 System.Collections.Generic.List`1/Enumerator<!T>::index
    L_0041: ldc.i4.1 
    L_0042: add 
    L_0043: stfld int32 System.Collections.Generic.List`1/Enumerator<!T>::index
    L_0048: ldc.i4.1 
    L_0049: ret 
    L_004a: ldarg.0 
    L_004b: call instance bool System.Collections.Generic.List`1/Enumerator<!T>::MoveNextRare()
    L_0050: ret 
}

这里没有必要阅读IL,通过Reflector,代码如下:
public bool MoveNext()
{
    List<T> list = this.list;
    if ((this.version == list._version) && (this.index < list._size))
    {
        this.current = list._items[this.index];
        this.index++;
        return true;
    }
    return this.MoveNextRare();
}
我们看到,在每次MoveNext的时候,List会检查当前枚举器对象的版本和list的版本是否一致,如果不一致就执行this.MoveNextRare()方法。
 
我们再关注一下Remove方法:
public void RemoveAt(int index)
{
    if (index >= this._size)
    {
        ThrowHelper.ThrowArgumentOutOfRangeException();
    }
    this._size--;
    if (index < this._size)
    {
        Array.Copy(this._items, index + 1, this._items, index, this._size - index);
    }
    this._items[this._size] = default(T);
    this._version++;
}

从该方法,没进行一次Remove操作,list的_version字段会自增1。
到这里,基本上咱们就该明白为什么最上面的代码执行不下去了。

结论:
  • 在遍历的同时,修改List对象,这样会抛出异常,这是因为List<T>对象和List<T>::Enumerator对象各自维护了一个版本字段,如果发现这两个版本字段不一致,就会中止遍历,抛出异常。
  • List<T>对象的本质是数组,而不是链表。
原文地址:https://www.cnblogs.com/quark/p/2158076.html