Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Different exception-throwing behavior of IEnumerator.Current and IEnumerator<T>.Current

Tags:

c#

ienumerator

I got an enumerator by calling IEnumerable<T>.GetEnumerator(), I then called it's MoveNext() until it returned false, and then accessed its Current property. To my surprise, no exception was thrown.

Digging MSDN, I found that the non-generic version will throw if Current is accessed after MoveNext() returned false, whereas the generic version will not.

Can someone explain this difference?

like image 998
bavaza Avatar asked Oct 02 '22 15:10

bavaza


2 Answers

The behavior of the generic enumerator is undefined, anything is possible and it is ultimately up to the collection type to define what undefined is going to mean.

But they can do something reasonable beyond throwing, the generic enumerators know the type of the collection object. So they can return default(T).

The non-generic enumerators don't have that luxury, they could only return null or new object(). ArrayList in fact has code that reserves a static object for just this purpose. But doesn't actually use it, looks like they changed their mind after usability testing. Returning either is going to make the client code fail with a very unpleasant exception, NullReferenceException or InvalidCastException. Exceptions that can also be raised in normal usage of those collections so there's very little hint to the actual cause of the mishap. So they don't, they throw InvalidOperationException instead.

like image 117
Hans Passant Avatar answered Oct 07 '22 19:10

Hans Passant


Indeed, this curiosity has been noted many times - but ultimately, after MoveNext() has returned false, you should consider the value of Current an undefined behaviour. If you do that: there is no issue here. Just: don't access Current here - then it doesn't matter whether it is "the last known value" vs "the default value of that type" vs "throws an exception".

Additionally, note that since the exception is thrown by the implementation, it is also entirely possible for an implementation of the generic API to throw, and for the non-generic API to not throw. As an example of the latter - iterator blocks do not throw here:

    static void Main()
    {
        IEnumerator<int> typed = GetInts();
        typed.MoveNext();
        Console.WriteLine(typed.MoveNext());
        int i = typed.Current;

        IEnumerator untyped = GetInts();
        untyped.MoveNext();
        Console.WriteLine(untyped.MoveNext());
        object o = untyped.Current;
    }

    static IEnumerator<int> GetInts()
    {
        yield return 4;
        yield break;
    }
like image 44
Marc Gravell Avatar answered Oct 07 '22 17:10

Marc Gravell