关于C#:为什么ControlCollection不引发InvalidOperationException?

Why does ControlCollection NOT throw InvalidOperationException?

在处理控件跳过迭代的foreach循环出现此问题之后,它向我发出了一个错误,即允许在更改的集合上进行迭代:

例如,以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
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: Collection was modified; enumeration operation may not execute.

但是,在.NET表单中,您可以执行以下操作:

1
2
3
4
5
6
7
8
9
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上的迭代不抛出invalidOperationException?

附录:

IEnumerator的文件中说:

The enumerator does not have exclusive access to the collection; therefore, enumerating through a collection is intrinsically not a thread-safe procedure. Even when a collection is synchronized, other threads can still modify the collection, which causes the enumerator to throw an exception.


答案可以在EDOCX1的参考源中找到。

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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()中明确提出这一点的评论。

在我看来,这是一个错误的"修复",因为它通过引入一个微妙的错误来掩盖一个明显的错误(如操作说明,元素被悄悄地跳过)。


在foreach控件c跳过控件的注释中引发了未引发异常的相同问题。这个问题使用了类似的代码,除了在调用Dispose()之前从Controls中显式删除了子Control

1
2
3
4
5
6
7
8
foreach (Control cntrl in Controls)
{
    if (cntrl.GetType() == typeof(Button))
    {
        Controls.Remove(cntrl);
        cntrl.Dispose();
    }
}

我可以通过文档来找到这种行为的解释。基本上,在枚举时修改任何集合都会导致抛出异常是一个错误的假设;这样的修改会导致未定义的行为,如果有,则取决于具体的集合类如何处理该场景。

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

If changes are made to the collection, such as adding, modifying, or deleting elements, the behavior of the enumerator is undefined.

Dictionary<>List<>Queue<>等类在枚举过程中被修改时被记录为抛出InvalidOperationException

An enumerator remains valid as long as the collection remains unchanged. If changes are made to the collection, such as adding, modifying, or deleting elements, the enumerator is irrecoverably invalidated and the next call to MoveNext or IEnumerator.Reset throws an InvalidOperationException.

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

较旧的集合类(如ArrayListHashtable)专门将此场景中的行为定义为超出枚举器无效范围的未定义行为…

An enumerator remains valid as long as the collection remains unchanged. If changes are made to the collection, such as adding, modifying, or deleting elements, the enumerator is irrecoverably invalidated and its behavior is undefined.

…虽然在测试中,我发现两个类的枚举器确实在失效后抛出了一个InvalidOperationException

与上面的类不同,Control.ControlCollection类既没有定义也没有对这种行为进行评论,因此上面的代码"仅仅"以一种微妙的、不可预测的方式失败,也没有明确表示失败的异常;它从来没有说它会显式失败。

因此,一般来说,在枚举期间修改集合可以保证(可能)失败,但不能保证引发异常。