【问题标题】:Why does ControlCollection NOT throw InvalidOperationException?为什么 ControlCollection 不抛出 InvalidOperationException?
【发布时间】:2016-01-29 12:21:22
【问题描述】:

在这个问题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 不会抛出 InvalidOperationException?

附录:

documentation for IEnumerator 说:

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

【问题讨论】:

  • 对于其他人来说,this is whereControl 在被处置时会从父集合中删除。
  • @Serv 但是你没有调用 Control.ControlCollection.Remove() ?
  • @ChrisWohlert 在处置过程中代表您调用/删除了它 - 请参阅我上面评论中的链接。
  • @AmitKumarGhosh 循环遍历已经被释放的子控件。出于所有意图和目的,孩子的父母已经消失,因此在调用孩子 dispose 之前将其设置为 null 是有意义的
  • @AmitKumarGhosh 父级中的代码将子级的父级设置为空。 IE 它正在与自己的孩子分离,因为它已经完成了一半的处置。鉴于发生的一件事是孩子在处置过程中在其父母的孩子集合上调用Remove,您可能希望首先断开该链接。

标签: c# .net winforms foreach invalidoperationexception


【解决方案1】:

这个问题的答案可以在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() 中明确解决此问题的 cmets。

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

【讨论】:

  • +1 所以这就解释了它是如何避免它的。但它不应该在那里抛出 InvalidOperationException 吗?文档将暗示任何 IEnumerator 都应该这样做。如果这个暗示是正确的,那么它是一个错误,对吧?但是鉴于源代码中的 cmets,它明确地避免了它。很奇怪。
  • @MeirionHughes 我同意 - 看起来他们试图解决一个感知到的问题,这样做会使事情变得更糟(如果你认为有一个无声的错误而不是一个响亮的错误更糟糕,我做)
  • 直到 ms-team 的某个人解释为什么他们明确不想抛出 InvalidOperationException,这将是公认的答案。 :)
  • 他们经常声称的设计选择。
【解决方案2】:

foreach control c# skipping controls 的 cmets 中引发了同样的not异常问题。该问题使用类似的代码,除了子 Control 在调用 Dispose() 之前从 Controls 中显式删除...

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

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

根据IEnumerable.GetEnumerator()IEnumerable&lt;&gt;.GetEnumerator()方法的备注...

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

Dictionary&lt;&gt;List&lt;&gt;Queue&lt;&gt; 等类被记录为在枚举期间修改时会抛出 InvalidOperationException...

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

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

较早的集合类,例如 ArrayListHashtable 专门将这种情况下的行为定义为未定义,超出枚举数无效...

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

...虽然在测试中我发现这两个类的枚举数实际上在失效后都会抛出 InvalidOperationException

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-03-20
    • 1970-01-01
    • 1970-01-01
    • 2015-08-27
    • 1970-01-01
    相关资源
    最近更新 更多