c# - 为什么编译器生成的 IEnumerator<T> 持有对创建它的实例的引用?

标签 c# .net garbage-collection ienumerable

在做一个项目时,我写了一个类似于下面的迭代器 block :

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 将能够收集原始集合,因为迭代器 block 不再持有对它的引用,只有它的尾部(如 collection = collection.Tail 中所示)。

然而,这并没有发生。我发现了编译器生成的 IEnumerator<T>始终持有对Sequence<T> 实例的引用创造了它。

为了证明这一点,我编写了以下迭代器 block 并检查了生成的 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

通过查看 <GetEnumerator>d__3 的 IL , 似乎是 <>4__this字段从未访问过。 那么为什么它会生成呢?为什么枚举器需要指向 Sequence<T> 的实例是什么创造了它?

我可以通过自己编写 IEnumerator<T> 来解决这个问题,但我仍然想知道为什么首先会发生这种情况。


如果你想自己编译这个,你可以从这里获取项目的源代码: https://github.com/dcastro/Sequences

这是原始的迭代器 block :

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 是有意义的(并且接受如果你正在调试一个不适合调试的构建,则有一些限制)但我想这只是编译器的优化作者认为不重要。

关于c# - 为什么编译器生成的 IEnumerator<T> 持有对创建它的实例的引用?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23564913/

相关文章:

java - 少量的Xmx是否会因为Garbage Collection而导致java程序运行效率低下?

silverlight - 为 Silverlight 行为自动调用 OnDetaching()

c# - 关于事件的值/引用类型的问题

c# - 将 char 转换为 unsigned short : what happens behind the scenes?

c# - 无法在 WinForms ComboBox 中更改 DisplayMember

c# - 将默认无参数构造函数添加到结构的解决方法

c# - 等于 NHibernate 实体的实现,unproxy 问题

c# - 在不提示用户的情况下删除 SSL 证书

asp.net - .NET 4.6.2 引入的 System.Web.Globalization 命名空间在运行时与 System.Globalization 发生冲突

java - PSYoungGen 空间错误