按照这个问题 Foreach 循环处理跳过迭代的控件,它让我觉得在不断变化的集合上允许迭代:
例如,以下内容:
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
上迭代不会抛出 InvalidOperationException?
附录:
IEnumerator 的文档说:
枚举器没有对集合的独占访问权; 因此,通过集合进行枚举本质上不是线程安全的过程。 即使在同步集合时,其他线程仍然可以修改集合,这会导致枚举器抛出异常。
答案可以在 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 所指出的,元素被悄悄地跳过)。
在 foreach 控件 c# skipping 控件的注释中提出了同样的未抛出异常问题。 该问题使用类似的代码,除了在调用Dispose()
之前从Controls
明确删除了子Control
...
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
指定了显式失败的行为。 因此,它是否因异常而失败取决于每个类。
较旧的集合类(例如 ArrayList 和 Hashtable)专门将这种情况下的行为定义为未定义的枚举数无效...
只要集合保持不变,枚举器就保持有效。 如果对集合进行了更改,例如添加、修改或删除元素,则枚举数将不可恢复地失效并且其行为未定义。
...虽然在测试中我发现这两个类的枚举器实际上在失效后会抛出InvalidOperationException
。
与上面的类不同,Control.ControlCollection 类既没有定义也没有评论这种行为,因此上面的代码“仅仅”以一种微妙的、不可预测的方式失败,没有例外地明确指示失败; 它从未说过它会明确失败。
因此,一般来说,在枚举期间修改集合肯定会(可能)失败,但不保证会引发异常。