为什么 ControlCollection 不抛出 InvalidOperationException?

Why does ControlCollection NOT throw InvalidOperationException?

在这个问题 之后,它让我感到困扰,因为允许对不断变化的集合进行迭代:

例如,以下内容:

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 表单中,您可以:

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?

附录:

documentation for 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.

这个问题的答案可以在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 这是一个被误导的 "fix" 因为它通过引入一个微妙的错误来掩盖一个明显的错误(元素被默默地跳过,正如 OP 所指出的)。

foreach control c# skipping controls 的评论中提出了同样的异常 not 被抛出的问题。该问题使用类似的代码,除了 child Control 在调用 Dispose()...

之前明确从 Controls 中删除
foreach (Control cntrl in Controls)
{
    if (cntrl.GetType() == typeof(Button))
    {
        Controls.Remove(cntrl);
        cntrl.Dispose();
    }
}

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

根据IEnumerable.GetEnumerator() and 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<>, and Queue<> are documented to throw an 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.

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

较旧的集合 classes 例如 ArrayList and Hashtable 明确将这种情况下的行为定义为未定义,超出了枚举器的无效...

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.

...尽管在测试中我发现两个 classes 的枚举器实际上在失效后抛出 InvalidOperationException

与上面的 classes 不同,Control.ControlCollection class 既没有定义也没有评论这种行为,因此上面的代码在 "merely" 中以一种微妙的、不可预测的方式失败,没有明确指出异常失败; 它从未说过它会明确失败。

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