Search code examples
c#.netxelementienumerator

IEnumerator<XElement> behaviour


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:

  • Why does it keep on printing <c> even though the list is exhausted
  • Why 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>

Solution

  • 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.

    I tested some other cases:

    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.