I use the following program to enumerate elements in the xml input. When the final element is reached
x.MoveNext()
returns false and this part is clear. My questions are:
<c>
even though the list is exhaustedWhy does x.Current
never equal null? That is what I would expect since XElement
is a reference type
static void Main(string[] args)
{
var x = GetXml();
while (true)
{
var isMoving = x.MoveNext();
Console.WriteLine($"Cursor moved: {isMoving}");
Console.WriteLine($"Current element is null: {x.Current == null}");
Console.WriteLine($"Current element: {x.Current}" );
}
}
static IEnumerator<XElement> GetXml()
{
return XElement.Parse(@"<x>
<a></a>
<b></b>
<c></c>
</x>").Elements().GetEnumerator();
}
Output:
Cursor moved: true
Current element is null: False
Current element: <a></a>
Cursor moved: true
Current element is null: False
Current element: <b></b>
Cursor moved: true
Current element is null: False
Current element: <c></c>
Cursor moved: False <- confusing bit
Current element is null: False
Current element: <c></c>
Why does x.Current never equal null?
Short answer: As Brian Kernighan said, "your compiler is the final authority on the language"1. It does what it does because the guys at Microsoft wrote the code that way. As we'll see below, that's left up to the implementer in this case. In the words of legendary computer scientist Ray Dorset2, "just do what you feel".
But there's still something a little bit interesting here. According to MSDN,
If the last call to MoveNext returned false, Current is undefined.
That's for the generic IEnumerator<T>
, the one you're using. And it's the answer to your question.
In some languages, for example JavaScript, "undefined" is a keyword. In others, it's a plain-English word used to describe something left open by the spec. If they say behavior in C or C# is "undefined", they mean that it's up to the implementer.
The above differs from the documented behavior of the nongeneric IEnumerator
:
If the last call to MoveNext returned false, calling Current throws an exception.
All it's telling you is that with generic IEnumerator<T>
, after MoveNext()
returns false, Current
can do whatever the implementer feels like making it do. It could return default(T)
(that's what I'd do), but it could return the last item instead. I would hesitate to make it throw an exception, since we've got more than one case of Microsoft .NET code (the one you found, and a couple of mine that you're about to see) that doesn't throw, and MSDN doesn't say it might, so take that to mean that it just doesn't.
public class Program
{
public static void Main()
{
//var x = GetEnumeratorNonGeneric(1);
//var x = GetEnumerator(1);
//var x = GetEnumeratorInt(1);
//var x = GetEnumeratorIntRangeSelect(1);
var x = GetEnumeratorIntList(1);
for (int i = 0; i < 2; ++i)
{
var isMoving = x.MoveNext();
Console.WriteLine($"Cursor moved: {isMoving}");
Console.WriteLine($"Current element is null: {x.Current == null}");
Console.WriteLine($"Current element: {x.Current}" );
}
}
static IEnumerator<String> GetEnumerator(int count)
{
return Enumerable.Range(1, count).Select(n => n.ToString()).GetEnumerator();
}
static IEnumerator<int> GetEnumeratorInt(int count)
{
return Enumerable.Range(1, count).GetEnumerator();
}
static IEnumerator<int> GetEnumeratorIntRangeSelect(int count)
{
return Enumerable.Range(1, count).Select(n => n).GetEnumerator();
}
static IEnumerator<int> GetEnumeratorIntList(int count)
{
return Enumerable.Range(1, count).ToList().GetEnumerator();
}
static IEnumerator GetEnumeratorNonGeneric(int count)
{
return new ArrayList(Enumerable.Range(1, count).Select(n => n.ToString()).ToArray()).GetEnumerator();
}
}
Output for GetEnumeratorNonGeneric()
:
Cursor moved: True
Current element is null: False
Current element: 1
Cursor moved: False
Run-time exception (line -1): Enumeration already finished.
Stack Trace:
[System.InvalidOperationException: Enumeration already finished.]
Output from GetEnumerator()
. This doesn't throw, but it does return default(T)
, null
in this case.
Cursor moved: True
Current element is null: False
Current element: 1
Cursor moved: False
Current element is null: True
Current element:
Output from GetEnumeratorInt()
. Enumerable.Range()
returns an enumerator that uses the last value for Current
:
Cursor moved: True
Current element is null: False
Current element: 1
Cursor moved: False
Current element is null: False
Current element: 1
With GetEnumeratorIntRangeSelect()
, we find that Select()
gets us an enumerator whose Current
returns default(T)
after MoveNext()
returns false. This is really redundant with GetEnumerator()
above, but it illustrates what happens with a value type rather than a reference type.
Cursor moved: True
Current element is null: False
Current element: 1
Cursor moved: False
Current element is null: False
Current element: 0
The List<T>
enumerator we get from calling ToList()
also returns default(T)
from Current
after the end.
I didn't find a generic IEnumerator<T>
that throws on Current
after the end, but as you can see I didn't make a great project of looking for one.
1 Or words to that effect; I can't find the exact wording.
2 Possibly not a real computer scientist.