Search code examples
c#.netiterator

IEnumerator<T>.Current beyond MoveNext() == false


I have a following code in my graph library:

while(true) {
    using (var i = ancestry.GetAdjacent(current).GetEnumerator())
    {
        if (!i.MoveNext())
            yield break;
    
        if (i.MoveNext())
            throw new InvalidOperationException("ancestry graph can only have one adjacent vertex for any given vertex");
        // it's a bug, as some enumerators could return null after failed MoveNext().
        current = i.Current;
        if (isSentinel(current))
            yield break;
    }
}

This code works for some instances of IEnumerator, and fails for others (all standard implementations from .NET, plus iterator methods with yield return.

Is that behaviour unspecified?

If not, what is the correct notation? Leave Current at previous, or set to default(T)?

UPDATE:

I have explored some of the implementations of IEnumerator for collections, Linq extensions as well as typed array: fiddle.

In my understanding, Linq violates the contract of IEnumerable, as it does not throw on Current beyond the end of enumeration, and typed array violates the contract of IEnumerable, as it throws on IEnumerable<T>.Current, and there are no exception types specified for IEnumerable.Current.

Can someone clarify, what does it mean 'undefined' when it's stated in documentation? Does it include throwing unspecified exceptions:

From IEnumerator.Current Property documentation:

Current is undefined under any of the following conditions:

  • The enumerator is positioned before the first element in the collection, immediately after the enumerator is created. MoveNext must be called to advance the enumerator to the first element of the collection before reading the value of Current.
  • The last call to MoveNext returned false, which indicates the end of the collection.
  • The enumerator is invalidated due to changes made in the collection, such as adding, modifying, or deleting elements. Current returns the same object until MoveNext is called. MoveNext sets Current to the next element.

Solution

  • The value of Current is not defined, if MoveNext returns false, as per the contract of the interface, as is described in its documentation. You should never rely on any given behavior. You simply shouldn't ever check the value of Current whenever MoveNext is false.

    If MoveNext passes the end of the collection, the enumerator is positioned after the last element in the collection and MoveNext returns false. When the enumerator is at this position, subsequent calls to MoveNext also return false. If the last call to MoveNext returned false, Current is undefined.

    (Note that IEnumerator says that Current should throw an exception in this situation, but in my experience most implementations don't bother to explicitly throw, they just expose an undefined value; this change is reflected in the newer IEnumerator<T> interface's documentation.)