为什么ControlCollection不会抛出InvalidOperationException异常?

13

在这个问题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();
}

迭代器在遍历一个正在改变的集合时会跳过元素,而不抛出异常

bug?迭代器不是应该在底层集合改变时抛出 InvalidOperationException 异常吗?

所以我的问题是:为什么在改变 ControlCollection 时的迭代没有抛出 InvalidOperationException 异常?

补充:

IEnumerator 的文档中说:

枚举器无法独占地访问集合;因此,通过集合进行枚举本质上不是线程安全的过程。即使集合已同步,其他线程仍然可以修改集合,这会导致枚举器抛出异常


3
对于其他人寻找的话,这里是一个 Control 在被处理时从父级集合中移除自身的位置。 - James Thorpe
2
@Serv 但是你没有调用 Control.ControlCollection.Remove() 吗? - Chris Wohlert
1
@ChrisWohlert 在 dispose 期间,它会被自动调用/移除 - 请参见我上面评论中的链接。 - James Thorpe
1
@AmitKumarGhosh 这是在循环遍历已经被处理的子控件。从所有意义上讲,子控件的父级已经不存在了,因此在调用子控件的处理之前将其设置为 null 是有意义的。 - James Thorpe
1
@AmitKumarGhosh,代码是在父组件中将子组件的父组件设置为null。也就是说,当父组件进行销毁的时候,它已经与自己的子组件解除了关联。考虑到其中一件事情是子组件在销毁期间调用其父组件的子组件集合Remove方法,您可能需要先断开该链接。 - James Thorpe
显示剩余6条评论
2个回答

10
这个问题的答案可以在控件集合枚举器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()中的注释,这些注释明确讨论了这个问题。
在我看来,这是一个误导性的“修复”,因为它掩盖了一个明显的错误,并引入了一个微妙的错误(正如OP所指出的那样,元素被静默地跳过)。

+1 这就解释了它是如何避免这个问题的。但是它不应该在那里抛出InvalidOperationException吗?文档似乎暗示任何IEnumerator都应该这样做。如果这个暗示是正确的,那么这就是一个bug,对吧?但鉴于源代码中的注释,它明确地避免了这种情况。非常奇怪。 - Meirion Hughes
@MeirionHughes 我同意 - 看起来他们试图修复一个被认为存在的问题,但却因此使情况变得更糟(如果你认为将一个喧闹的错误变成一个无声的错误是更糟的话,我是这么认为的) - Matthew Watson
除非有MS团队的人明确解释为什么他们不想抛出“InvalidOperationException”,否则这将是被接受的答案。 :) - Meirion Hughes
作为他们经常强调的设计选择。 - Amit Kumar Ghosh

3

foreach control c# skipping controls的评论中,也提到了同样的异常未被抛出的问题。那个问题使用了类似的代码,不同之处在于在调用Dispose()之前,子Control被明确地从Controls中移除了...

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

我仅通过文档就找到了这种行为的解释。基本上,假设在枚举期间修改任何集合都会导致抛出异常是不正确的;这样的修改会导致未定义的行为,并且由特定的集合类处理该场景(如果有的话)。
根据IEnumerable.GetEnumerator()IEnumerable<>.GetEnumerator()方法的注释...

如果对集合进行更改,例如添加、修改或删除元素,则枚举器的行为是未定义的。

Dictionary<>List<>Queue<>这样的类在枚举时被修改会抛出一个InvalidOperationException...

只要集合保持不变,枚举器就保持有效。如果对集合进行更改,例如添加、修改或删除元素,则枚举器将被无法恢复地失效,并且对MoveNext或IEnumerator.Reset的下一次调用会抛出InvalidOperationException。

值得注意的是,上述每个类都指定了明确的失败行为,即通过InvalidOperationException抛出异常。因此,是否引发异常取决于每个类。
旧的集合类(例如ArrayListHashtable)特别将此场景的行为定义为未定义,超出了枚举器被失效这一点...

只要集合保持不变,枚举器就保持有效。如果对集合进行更改,例如添加、修改或删除元素,则枚举器将被无法恢复地失效,并且其行为是未定义的。

...尽管在测试中,我发现这两个类的枚举器在失效后确实会抛出InvalidOperationException
与上述类不同,Control.ControlCollection 既没有定义也没有注释这种行为,因此以上代码会以“仅仅”一种微妙、不可预测的方式失败,而没有明确指示失败的异常;它从未表示过它会明确地失败。 因此,通常情况下,在枚举期间修改集合保证(可能)会失败,但不保证会抛出异常。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接