繁体   English   中英

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

[英]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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM