0

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>
Pasato
  • 153
  • 9
  • For future reference, the purpose of tags here is to call attention to your question. Your question will only have been seen by people who monitor the "xelement" and "ienumerator" tags -- approximately nobody. I saw it by chance in the raw new question list, but so many questions come in that it's unlikely that anybody will notice any particular one. If you'd tagged it C# (I took the liberty), you'd have gotten it in front of the people likely to answer it. I realize this seems a bit counterintuitive, but it's the way the site has ended up being used. – 15ee8f99-57ff-4f92-890c-b56153 May 16 '17 at 14:21
  • Thanks for advice Ed. I'll bear it mind. I'm lucky you've stumbled upon my question cause your answer is perfect :) – Pasato May 18 '17 at 12:45

1 Answers1

1

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.