![](/img/trans.png)
[英]When using “yield” why does compiler-generated type implement both IEnumerable and IEnumerator
[英]Why does a compiler-generated IEnumerator<T> hold a reference to the instance that created it?
在处理项目时,我编写了一个类似于以下内容的迭代器块:
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();
}
}
正如你所看到的,我预计在第二次调用MoveNext
,GC将能够收集原始集合,因为迭代器块不再持有对它的引用,只有它的尾部(如collection = collection.Tail
)。
但是,这没有发生。 我发现编译器生成的IEnumerator<T>
将始终保存对创建它的Sequence<T>
实例的引用。
为了证明这一点,我编写了以下迭代器块并检查了生成的IL:
public IEnumerator<T> GetEnumerator()
{
yield return default(T);
}
令我惊讶的是,IL相当于:
public IEnumerator<T> GetEnumerator()
{
var enumerator = new CompilerGeneratedEnumerator();
enumerator.this_field = this;
}
逐字:
.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
通过查看IL获取<GetEnumerator>d__3
,似乎<>4__this
字段永远不会被访问。 那么为什么它会产生呢? 为什么枚举器需要指向创建它的Sequence<T>
的实例?
我能够通过编写自己的IEnumerator<T>
解决这个问题,但我仍然想知道为什么会发生这种情况。
如果你想自己编译,你可以从这里获取项目的来源: https : //github.com/dcastro/Sequences
这是原始的迭代器块:
ISequence<T> sequence = this;
while (!sequence.IsEmpty)
{
yield return sequence.Head;
sequence = sequence.Tail;
}
从逻辑上讲,您的第一个方法应该捕获this
这一行:
Sequence<T> collection = this;
...将仅在第一次调用MoveNext()
,因此它确实需要捕获它,并且它只能在生成的代码中的实例变量中捕获它。 编译器可以在最终使用后显式地将其清空,但通常这只会是浪费。
现在你的第二个案例更有趣了。 是的,为了完成它并不需要参考的方法this
-但如果你是在一个调试器,而你又对断点yield return
的语句,你会希望能够检查this
,因为你”在实例方法中。 因此,至少在调试信息和无优化构建,我认为这是合理的,包括this
作为一个实例变量。 在优化的构建中,有意义的是不捕获this
(并接受如果您正在调试不适合调试的构建,则存在一些限制)但我想这只是编译器作者认为不重要的优化。
如果在迭代继续之后原始项是GCable很重要,那么您可以自己实现IEnumerator<T>
,而不是让编译器为您生成一个。
以下编译,但可能会改进:
public class MyCollection<T> : IEnumerable<T>
{
private T Head;
private MyCollection<T> Tail;
private bool IsEmpty;
private class ThisEnumerator : IEnumerator<T>
{
public ThisEnumerator(MyCollection<T> toIterate)
{
_innerCurrent = toIterate;
}
private MyCollection<T> _innerCurrent;
private bool _hasMoved = false;
public T Current
{
get
{
return _innerCurrent.Head;
}
}
object IEnumerator.Current
{
get
{
return this.Current;
}
}
public void Dispose()
{
_innerCurrent = null;
}
public bool MoveNext()
{
if (_hasMoved)
{
_innerCurrent = _innerCurrent.Tail;
}
else
{
_hasMoved = true;
}
return !_innerCurrent.IsEmpty;
}
public void Reset()
{
throw new NotSupportedException();
}
}
public IEnumerator<T> GetEnumerator()
{
return new ThisEnumerator(this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}
关于“为什么”的问题,正如我在评论中指出的那样,我认为大多数迭代器(以及共享大量此类机制的async
块) 可能需要访问this
。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.