为什么ControlCollection不会抛出InvalidOperationException?

时间:2016-01-29 12:21:22

标签: c# .net winforms foreach invalidoperationexception

关注这个问题Foreach loop for disposing controls skipping iterations它告诉我,在更改的集合中允许迭代:

例如,以下内容:

List<Control> items = new List<Control>
{
    new TextBox {Text = "A", Top = 10},
    new TextBox {Text = "B", Top = 20},
    new TextBox {Text = "C", Top = 30},
    new TextBox {Text = "D", Top = 40},
};

foreach (var item in items)
{
    items.Remove(item);
}

抛出

  

InvalidOperationException:修改了集合;枚举操作可能无法执行。

但是在.Net表单中,您可以这样做:

this.Controls.Add(new TextBox {Text = "A", Top = 10});
this.Controls.Add(new TextBox {Text = "B", Top = 30});
this.Controls.Add(new TextBox {Text = "C", Top = 50});
this.Controls.Add(new TextBox {Text = "D", Top = 70});

foreach (Control control in this.Controls)
{
    control.Dispose();
}

跳过元素,因为迭代器在更改的集合上运行,而不会抛出异常

错误?如果底层集合发生变化,则抛出InvalidOperationException所需的迭代器不是?

所以我的问题是为什么迭代更改ControlCollection NOT抛出InvalidOperationException?

附录:

documentation for IEnumerator说:

  

枚举器没有对集合的独占访问权限;因此,枚举通过集合本质上不是一个线程安全的过程。即使集合是同步的,其他线程仍然可以修改集合,会导致枚举器抛出异常

2 个答案:

答案 0 :(得分:9)

可以在the Reference Source for ControlCollectionEnumerator

中找到答案
private class ControlCollectionEnumerator : IEnumerator {
    private ControlCollection controls; 
    private int current;
    private int originalCount;

    public ControlCollectionEnumerator(ControlCollection controls) {
        this.controls = controls;
        this.originalCount = controls.Count;
        current = -1;
    }

    public bool MoveNext() {
        // VSWhidbey 448276
        // We have to use Controls.Count here because someone could have deleted 
        // an item from the array. 
        //
        // this can happen if someone does:
        //     foreach (Control c in Controls) { c.Dispose(); }
        // 
        // We also dont want to iterate past the original size of the collection
        //
        // this can happen if someone does
        //     foreach (Control c in Controls) { c.Controls.Add(new Label()); }

        if (current < controls.Count - 1 && current < originalCount - 1) {
            current++;
            return true;
        }
        else {
            return false;
        }
    }

    public void Reset() {
        current = -1;
    }

    public object Current {
        get {
            if (current == -1) {
                return null;
            }
            else {
                return controls[current];
            }
        }
    }
}

特别注意MoveNext()中明确解决此问题的评论。

IMO这是一个被误导的“修复”,因为它通过引入一个微妙的错误掩盖了一个明显的错误(如OP所述,元素被默默地跳过)。

答案 1 :(得分:3)

foreach control c# skipping controls的注释中引发了抛出异常 not 的同一问题。该问题使用类似的代码,除了在调用Control之前将子ControlsDispose()中显式删除了。

foreach (Control cntrl in Controls)
{
    if (cntrl.GetType() == typeof(Button))
    {
        Controls.Remove(cntrl);
        cntrl.Dispose();
    }
}

我仅通过文档就能找到有关此行为的解释。基本上,在枚举时修改 any 集合总是会引发异常是一个错误的假设;这样的修改会导致未定义的行为,具体取决于集合类如何处理这种情况。

根据IEnumerable.GetEnumerator()IEnumerable<>.GetEnumerator()方法的说明...

  

如果对集合进行了更改(例如添加,修改或删除元素),则枚举器的行为是不确定的。

据记录,诸如Dictionary<>List<>Queue<>之类的类在枚举期间被修改时会抛出InvalidOperationException

  

只要集合保持不变,枚举数将保持有效。如果对集合进行了更改(例如添加,修改或删除元素),则枚举数将不可避免地无效,并且下次调用MoveNext或IEnumerator.Reset会引发InvalidOperationException。

值得一提的是,上面提到的每个类(而不是它们都实现的接口)指定了通过InvalidOperationException进行显式失败的行为。因此,是否因异常而失败取决于每个类。

诸如ArrayListHashtable之类的较旧的收集类专门将这种情况下的行为定义为未定义,超出了使枚举器无效的范围……

  

只要集合保持不变,枚举数将保持有效。如果对集合进行了更改(例如添加,修改或删除元素),则枚举数将无法恢复,并且其行为是不确定的。

...尽管在测试中,我发现两个类的枚举数实际上在无效后都会抛出InvalidOperationException

与上述类不同,Control.ControlCollection class既没有定义也没有注释这种行为,因此上述代码“仅”以一种微妙的,不可预测的方式失败,没有任何异常明确表明失败; 从没说过会明确失败。

因此,通常,在枚举期间修改集合肯定会(很可能)失败,但不会保证会引发异常。