Why does ControlCollection NOT throw InvalidOperationException?

安稳与你 提交于 2020-01-23 04:39:07

问题


Following this question Foreach loop for disposing controls skipping iterations it bugged me that iteration was allowed over a changing collection:

For example, the following:

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);
}

throws

InvalidOperationException: Collection was modified; enumeration operation may not execute.

However in a .Net Form you can do:

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();
}

which skips elements because the the iterator runs over a changing collection, without throwing an exception

bug? aren't iterators required to throw InvalidOperationException if the underlaying collection changes?

So my question is Why does iteration over a changing ControlCollection NOT throw InvalidOperationException?

Addendum:

The documentation for IEnumerator says:

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.


回答1:


The answer to this can be found in 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];
            }
        }
    }
}

Pay particular attention to the comments in MoveNext() which explicitly address this.

IMO this is a misguided "fix" because it masks an obvious error by introducing a subtle one (elements are silently skipped, as noted by the OP).




回答2:


This same issue of an exception not being thrown was raised in the comments on foreach control c# skipping controls. That question uses similar code except the child Control is explicitly removed from Controls before calling Dispose()...

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

I was able to find an explanation for this behavior through documentation alone. Basically, that modifying any collection while enumerating always causes an exception to be thrown is an incorrect assumption; such a modification causes undefined behavior, and it's up to the specific collection class how to handle that scenario, if at all.

According to the remarks for the IEnumerable.GetEnumerator() and IEnumerable<>.GetEnumerator() methods...

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

Classes such as Dictionary<>, List<>, and Queue<> are documented to throw an InvalidOperationException when modified during enumeration...

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.

It's worth calling attention to the fact that it's each class I mentioned above, not the interfaces they all implement, that specifies the behavior of explicit failure via an InvalidOperationException. Thus, it's up to each class whether it fails with an exception or not.

Older collection classes such as ArrayList and Hashtable specifically define the behavior in this scenario as undefined beyond the enumerator being invalidated...

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.

...although in testing I found that enumerators for both classes do, in fact, throw an InvalidOperationException after being invalidated.

Unlike the above classes, the Control.ControlCollection class neither defines nor comments on such behavior, hence the above code failing in "merely" a subtle, unpredictable way with no exception explicitly indicating failure; it never said it would explicitly fail.

So, in general, modifying a collection during enumeration is guaranteed to (likely) fail, but not guaranteed to throw an exception.



来源:https://stackoverflow.com/questions/35084463/why-does-controlcollection-not-throw-invalidoperationexception

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!