I would have thought that executing the following code for an empty collection that implements IEnumerable<T>
would throw an exception:
var enumerator = collection.GetEnumerator();
enumerator.MoveNext();
var type = enumerator.Current.GetType(); // Surely should throw?
Because the collection is empty, then accessing IEnumerator.Current
is invalid, and I would have expected an exception. However, no exception is thrown for List<T>
.
This is allowed by the documentation for IEnumerator<T>.Current
, which states that 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.
(I'm assuming that "fails to throw an exception" can be categorised as "undefined behaviour"...)
However, if you do the same thing but use an IEnumerable
instead, you DO get an exception. This behaviour is specified by the documentation for IEnumerator.Current
, which states:
- Current should throw an InvalidOperationException if the last call to MoveNext returned false, which indicates the end of the collection.
My question is: Why this difference? Is there a good technical reason that I'm unaware of?
It means identical-seeming code can behave very differently depending on whether it's using IEnumerable<T>
or IEnumerable
, as the following program demonstrates (note how the code inside showElementType1()
and showElementType1()
is identical):
using System;
using System.Collections;
using System.Collections.Generic;
namespace ConsoleApplication2
{
class Program
{
public static void Main()
{
var list = new List<int>();
showElementType1(list); // Does not throw an exception.
showElementType2(list); // Throws an exception.
}
private static void showElementType1(IEnumerable<int> collection)
{
var enumerator = collection.GetEnumerator();
enumerator.MoveNext();
var type = enumerator.Current.GetType(); // No exception thrown here.
Console.WriteLine(type);
}
private static void showElementType2(IEnumerable collection)
{
var enumerator = collection.GetEnumerator();
enumerator.MoveNext();
var type = enumerator.Current.GetType(); // InvalidOperationException thrown here.
Console.WriteLine(type);
}
}
}
The problem with
IEnumerable<T>
is thatCurrent
is of typeT
. Instead of throwing an exception,default(T)
is returned (it is set fromMoveNextRare
).When using
IEnumerable
you don't have the type, and you can't return a default value.The actual problem is you don't check the return value of
MoveNext
. If it returnsfalse
, you shouldn't callCurrent
. The exception is okay. I think they found it more convenient to returndefault(T)
in theIEnumerable<T>
case.Exception handling brings overhead, returning
default(T)
doesn't (that much). Maybe they just thought there was nothing useful to return from theCurrent
property in the case ofIEnumerable
(they don't know the type). That problem is 'solved' inIEnumerable<T>
when usingdefault(T)
.According to this bug report (thanks Jesse for commenting):
This could point in the direction of the overhead of exception handling. Or the required extra step to validate the value of
current
.They effectively just wave the responsibility to
foreach
, since that is the main user of the enumerator: