Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    primarykey
    data
    text
    <p>I feel like a quick recap of iterators is needed.</p> <p>An iterator (IEnumerator and IEnumerable for C#) is used to access elements of a structure in an ordered manner without exposing the underlying representation. The consequence is that it allows you to have extemely generic functions such as the following.</p> <pre><code>void Iterator&lt;T, V&gt;(T collection, Action&lt;V&gt; actor) where T : IEnumerable&lt;V&gt; { foreach (V value in collection) actor(value); } //Or the more verbose way void Iterator&lt;T, V&gt;(T collection, Action&lt;V&gt; actor) where T : IEnumerable&lt;V&gt; { using (var iterator = collection.GetEnumerator()) { while (iterator.MoveNext()) actor(iterator.Current); } } //Or if you need to support non-generic collections (ArrayList, Queue, BitArray, etc) void Iterator&lt;T, V&gt; (T collection, Action&lt;V&gt; actor) where T : IEnumerable { foreach (object value in collection) actor((V)value); } </code></pre> <p>There are trade-offs, as can be seen in the C# specification.</p> <p>5.3.3.16 Foreach statements</p> <blockquote> <p>foreach ( type identifier in expr ) embedded-statement</p> <ul> <li><p>The definite assignment state of v at the beginning of expr is the same as the state of v at the beginning of stmt.</p></li> <li><p>The definite assignment state of v on the control flow transfer to embedded-statement or to the end point of stmt is the same as the state of v at the end of expr.</p></li> </ul> </blockquote> <p>Which simply means that values are read-only. Why are they read-only? It's simple. Since <code>foreach</code> is such a high level statement, it can't and won't assume anything about the container you are iterating over. What if you were iterating over a binary tree and decided to randomly assign values inside the foreach statement. If <code>foreach</code> didn't force read-only access then your binary tree would degenerate into a tree. The entire data structure would be in disarray. </p> <p>But this wasn't your original question. You were modifying the collection before you even accessed the first element and an error was thrown. Why? For this, I dug into the List class using <a href="http://wiki.sharpdevelop.net/ILSpy.ashx" rel="noreferrer">ILSpy</a>. Here's a snippet of the List class</p> <pre><code>public class List&lt;T&gt; : IList&lt;T&gt;, ICollection&lt;T&gt;, IEnumerable&lt;T&gt;, IList, ICollection, IEnumerable { private int _version; public struct Enumerator : IEnumerator&lt;T&gt;, IDisposable, IEnumerator { private List&lt;T&gt; list; private int version; private int index; internal Enumerator(List&lt;T&gt; list) { this.list = list; this.version = list._version; this.index = 0; } /* All the implemented functions of IEnumerator&lt;T&gt; and IEnumerator will throw a ThrowInvalidOperationException if (this.version != this.list._version) */ } } </code></pre> <p>The enumerator is initialized with the parent list's "version" and a reference to the parent list. <em>All</em> iterating operations check to make sure that the initial version is equivalent to the referenced list's current version. If they are out of synch, the iterator is no longer valid. Why does the BCL do this? Why didn't the implementers check if the index of the enumerator was 0 (representing a new enumerator), and if it was, simply resynch the versions? I'm not sure. I can only hypothesize that the team wanted conformity amongst all the classes that implemented IEnumerable and they also wanted to keep it simple. Therefore a List's enumerator (and I believe most others) do not discriminate between elements as long as they are within range. </p> <p>This is the root cause of your problem. If you absolutely must have this functionality, then you will have to implement your own iterator and you may end up having to implement your own List. In my opinion, way too much work to against the flow of the BCL.</p> <p>Here's a quote from the <a href="http://en.wikipedia.org/wiki/Design_Patterns" rel="noreferrer">GoF</a> when designing an iterator that the BCL team probably followed:</p> <blockquote> <p>It can be dangerous to modify an aggregate while you're traversing it. If elements are added or deleted from the aggregate, you might end up accessing an element twice or missing it completely. A simple solution is to copy the aggregate and traverse the copy, but that's too expensive to do in general</p> </blockquote> <p>The BCL team most likely decided that it was too expensive in space-time complexity and manpower. And this philosophy is seen throughout C#. It is probably too expensive to allow modification of variables inside a foreach, too expensive to have List's Enumerator discriminate where it's in the list, and too expensive to cradle the user. Hopefully I've explained it well enough that one can seen the power and the constraint of iterators.</p> <p><strong>Reference</strong>:</p> <p>What changes the "version" of a list and thus invalidates all current enumerators?</p> <ul> <li>Changing an element through the indexer</li> <li><code>Add</code></li> <li><code>AddRange</code></li> <li><code>Clear</code></li> <li><code>Insert</code></li> <li><code>InsertRange</code></li> <li><code>RemoveAll</code></li> <li><code>RemoveAt</code></li> <li><code>RemoveRange</code></li> <li><code>Reverse</code></li> <li><code>Sort</code></li> </ul>
    singulars
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. VO
      singulars
      1. This table or related slice is empty.
    2. VO
      singulars
      1. This table or related slice is empty.
    3. VO
      singulars
      1. This table or related slice is empty.
    1. COI recognize that it is reasonable to require that a non-broken `IEnumerator<T>` must not behave in wonky fashion if a collection is modified during enumeration, and that if an enumerator can't return things with reasonable semantics, the best alternative is to throw an exception (though a distinct exception type should have been used for that purpose, to distinguish it from the case where an `InvalidOperationException` occurs for some reason unrelated to a modified collection). I dislike the notion that an exception is the "preferred" behavior, however.
      singulars
    2. COBesides throwing an exception, what would another course of action be? I can only think of adding in a Valid property flag, but that would have side effects of it own. I believe that since trying to modify the current enumerated value results in a compiler error, it makes sense that if the underlying structure was modified an exception is thrown if the enumeration is continued.
      singulars
    3. COAnother course of action would be to continue the enumeration, with the guarantees that any item which existed throughout the enumeration would be returned exactly once, and any item which existed for part of the enumeration would be returned at most once. Some types of collection would have difficulty making such guarantees (and throwing an exception would be appropriate in such cases), but it can be useful for collections to offer such guarantees. How useful would the `GetEnumerator` method of `ConcurrentDictionary` be if the enumeration died any time the collection was changed?
      singulars
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload