6

Despite the fact, that IEnumerator.Reset method should never be used I found strange behavior of the method implementation within List<T>.

No matter how you examine the .NET Framework Source Code (tried with reference source and ILSpy) the method is implemented as following:

void System.Collections.IEnumerator.Reset() {
    if (version != list._version) {
        ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
    }

    index = 0;
    current = default(T);
}

However, it looks like the method is never called at all! Consider the code:

var list = new List<int>(1) { 3 };
using (var e = list.GetEnumerator())
{
    Console.WriteLine(e.MoveNext());
    Console.WriteLine(e.Current);

    ((IEnumerator)e).Reset();

    Console.WriteLine(e.MoveNext());
    Console.WriteLine(e.Current);
}

It's pretty clear, that it should print True and 3 twice. Instead of that the result is

True
3
False
0

Any simple explanation I'm missing?

Community
  • 1
  • 1
MarcinJuraszek
  • 124,003
  • 15
  • 196
  • 263
  • Turns out to be a new example of why mutable structs are evil. Mutable structs where the mutation happens through an explicit interface implementation are just a bit extra evil. But these nested `Enumerator` structs are normally only used by the C# compiler when it translates a `foreach` statement. – Jeppe Stig Nielsen Sep 30 '13 at 18:05

1 Answers1

15

Any simple explanation I'm missing?

Yes: you're boxing the List.Enumerator here:

((IEnumerator)e).Reset();

That takes a copy of the existing one and resets it - leaving the original in one piece.

To reset the actual enumerator, you'd need something like this:

var list = new List<int>(1) { 3 };
var e = list.GetEnumerator();
// Can't use "ref" with a using statement
try
{
    Console.WriteLine(e.MoveNext());
    Console.WriteLine(e.Current);

    Reset(ref e);

    Console.WriteLine(e.MoveNext());
    Console.WriteLine(e.Current);
}
finally
{
    e.Dispose();
}

static void Reset<T>(ref T enumerator) where T : IEnumerator
{
    enumerator.Reset();
}

It's tricky because it uses explicit interface implementation.

I haven't tested it, but I think that should work for you. Obviously it's a bad idea to do this...

EDIT: Alternatively, just change your variable type to IEnumerator or IEnumerator<int> to start with. Then it will be boxed once, and the Reset method will mutate the boxed value:

var list = new List<int>(1) { 3 };
using (IEnumerator e = list.GetEnumerator())
{
    Console.WriteLine(e.MoveNext());
    Console.WriteLine(e.Current);

    e.Reset();

    Console.WriteLine(e.MoveNext());
    Console.WriteLine(e.Current);
}
Servy
  • 202,030
  • 26
  • 332
  • 449
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • you also need to take it out of the `using` to pass it by reference. Other than that change, I tested it real quick and this does work. – Servy Sep 30 '13 at 17:55
  • 1
    @Servy is right. He can simply box it in the beginning and then re-use the same box all the way. Edit: For example if he first upcasts the `List<>` to the interface type `IEnumerable<>` or `IEnumerable`, then uses that interface's `GetEnumerator()` method, the enumerator will already be in a box when he gets it, and he can re-use that box through the entire example. – Jeppe Stig Nielsen Sep 30 '13 at 17:56
  • Sure, You're right. I've just tested it with assigning `IEnumerator` to variable and it is actually reset. Thanks. I'll mark the answer as accepted asap. – MarcinJuraszek Sep 30 '13 at 17:56
  • @JeppeStigNielsen: Yes, I thought of that just as I was leaving the house :) Will edit it in. – Jon Skeet Sep 30 '13 at 18:08