While working on a project, I wrote an iterator block similar to the following:
public class Sequence<T> : IEnumerable<T>
{
public T Head{get; private set;}
public Sequence<T> Tail {get; private set;}
public bool IsEmpty {get; private set;}
public IEnumerator<T> GetEnumerator()
{
Sequence<T> collection = this;
while (!collection.IsEmpty)
{
yield return collection.Head;
collection = collection.Tail;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
As you can see, I expected that after the second call to MoveNext
, the GC would be able to collect the original collection, since the iterator block no longer holds a reference to it, only its tail (as seen in collection = collection.Tail
).
However, this did not happen. I discovered the compiler-generated IEnumerator<T>
will always hold a reference to the instance of Sequence<T>
that created it.
To prove this, I wrote the following iterator block and inspected the generated IL:
public IEnumerator<T> GetEnumerator()
{
yield return default(T);
}
To my surprise, the IL was equivalent to this:
public IEnumerator<T> GetEnumerator()
{
var enumerator = new CompilerGeneratedEnumerator();
enumerator.this_field = this;
}
Verbatim:
.maxstack 2
.locals init (
[0] class Sequences.Sequence`1/'<GetEnumerator>d__3'<!T>
)
IL_0000: ldc.i4.0
IL_0001: newobj instance void class Sequences.Sequence`1/'<GetEnumerator>d__3'<!T>::.ctor(int32)
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: ldarg.0
IL_0009: stfld class Sequences.Sequence`1<!0> class Sequences.Sequence`1/'<GetEnumerator>d__3'<!T>::'<>4__this'
IL_000e: ldloc.0
IL_000f: ret
By looking at the IL for <GetEnumerator>d__3
, it seems the <>4__this
field is never accessed. So why is it generated anyway? Why does the enumerator need to point to the instance of Sequence<T>
that created it?
I was able to get around this problem by writing my own IEnumerator<T>
, but I'm still wondering why this happens in the first place.
If you want to compile this yourself, you can grab the project's source from here:
https://github.com/dcastro/Sequences
And here's the original iterator block:
ISequence<T> sequence = this;
while (!sequence.IsEmpty)
{
yield return sequence.Head;
sequence = sequence.Tail;
}