Why enumerator structs are a really bad idea (redux)
- by Simon Cooper
My previous blog post went into some detail as to why calling MoveNext on a BCL generic collection enumerator didn't quite do what you thought it would. This post covers the Reset method.
To recap, here's the simple wrapper around a linked list enumerator struct from my previous post (minus the readonly on the enumerator variable):
sealed class EnumeratorWrapper : IEnumerator<int> {
private LinkedList<int>.Enumerator m_Enumerator;
public EnumeratorWrapper(LinkedList<int> linkedList) {
m_Enumerator = linkedList.GetEnumerator();
}
public int Current {
get { return m_Enumerator.Current; }
}
object System.Collections.IEnumerator.Current {
get { return Current; }
}
public bool MoveNext() {
return m_Enumerator.MoveNext();
}
public void Reset() {
((System.Collections.IEnumerator)m_Enumerator).Reset();
}
public void Dispose() {
m_Enumerator.Dispose();
}
}
If you have a look at the Reset method, you'll notice I'm having to cast to IEnumerator to be able to call Reset on m_Enumerator. This is because the implementation of LinkedList<int>.Enumerator.Reset, and indeed of all the other Reset methods on the BCL generic collection enumerators, is an explicit interface implementation.
However, IEnumerator is a reference type. LinkedList<int>.Enumerator is a value type. That means, in order to call the reset method at all, the enumerator has to be boxed. And the IL confirms this:
.method public hidebysig newslot virtual final instance void Reset() cil managed
{
.maxstack 8
L_0000: nop
L_0001: ldarg.0
L_0002: ldfld valuetype
[System]System.Collections.Generic.LinkedList`1/Enumerator<int32>
EnumeratorWrapper::m_Enumerator
L_0007: box
[System]System.Collections.Generic.LinkedList`1/Enumerator<int32>
L_000c: callvirt instance void [mscorlib]System.Collections.IEnumerator::Reset()
L_0011: nop
L_0012: ret
}
On line 0007, we're doing a box operation, which copies the enumerator to a reference object on the heap, then on line 000c calling Reset on this boxed object. So m_Enumerator in the wrapper class is not modified by the call the Reset. And this is the only way to call the Reset method on this variable (without using reflection).
Therefore, the only way that the collection enumerator struct can be used safely is to store them as a boxed IEnumerator<T>, and not use them as value types at all.