dispose 是通过 yield return 调用到底层的吗?

Is dispose called down to the bottom via yield return?

为了简单起见,我给出了愚蠢的例子。

IEnumerable<T> Silly<T>(this IEnumerable<T> source)
{
    foreach(var x in source) yield return x;
}

我知道这会被编译成一个状态机。但它也类似于

IEnumerable<T> Silly<T>(this IEnumerable<T> source)
{
    using(var sillier = source.GetEnumerator())
    {
        while(sillier.MoveNext()) yield return sillier.Current;
    }
}

现在考虑这种用法

list.Silly().Take(2).ToArray();

这里可以看到Silly enumerable 可能没有被完全消耗,但是Take(2) itself 会被完全消耗。

问题: 当在 Take 枚举器上调用 dispose 时,它​​是否也会在 Silly 枚举器上调用 dispose,更具体地说是在 sillier 枚举器上调用 dispose?

我的猜测是,由于 foreach,编译器可以处理这个简单的用例,但是不那么简单的用例呢?

IEnumerable<T> Silly<T>(this IEnumerable<T> source)
{
    using(var sillier = source.GetEnumerator())
    {
        // move next can be called on different stages.
    }
}

这会成为问题吗?因为大多数枚举器不使用非托管资源,但如果有人这样做,可能会导致内存泄漏。


如果不调用 dispose,如何使一次性可枚举?


一个想法:每个yield return后面可以有一个if(disposed) yield break;。现在 dispose 愚蠢枚举器的方法只需要设置 disposed = true 并移动枚举器一次以释放所有需要的东西。

C# 编译器在将您的迭代器转换为实际代码时会为您处理 很多。例如,这里的 MoveNext 包含第二个示例的实现 1:

private bool MoveNext()
{
    try
    {
        switch (this.<>1__state)
        {
            case 0:
                this.<>1__state = -1;
                this.<sillier>5__1 = this.source.GetEnumerator();
                this.<>1__state = -3;
                while (this.<sillier>5__1.MoveNext())
                {
                    this.<>2__current = this.<sillier>5__1.Current;
                    this.<>1__state = 1;
                    return true;
                Label_005A:
                    this.<>1__state = -3;
                }
                this.<>m__Finally1();
                this.<sillier>5__1 = null;
                return false;

            case 1:
                goto Label_005A;
        }
        return false;
    }
    fault
    {
        this.System.IDisposable.Dispose();
    }
}

因此,您会注意到 using 中的 finally 子句根本不存在,它是一个状态机2依赖于处于某些良好 (>= 0) 状态才能取得进一步的进展。 (这也是非法的 C#,但是嘿嘿)。

现在让我们看看它的 Dispose:

[DebuggerHidden]
void IDisposable.Dispose()
{
    switch (this.<>1__state)
    {
        case -3:
        case 1:
            try
            {
            }
            finally
            {
                this.<>m__Finally1();
            }
            break;
    }
}

所以我们可以看到 <>m__Finally1 在这里被调用(以及由于在 MoveNext.

中退出 while 循环

<>m__Finally1

private void <>m__Finally1()
{
    this.<>1__state = -1;
    if (this.<sillier>5__1 != null)
    {
        this.<sillier>5__1.Dispose();
    }
}

因此,我们可以看到 sillier 已被处置 并且 我们进入了否定状态,这意味着 MoveNext 不必执行 任何特殊工作来处理"we've already been disposed state".

所以,

An Idea: there can be a if(disposed) yield break; after every yield return. now dispose method of silly enumerator will just have to set disposed = true and move the enumerator once to dispose all the required stuff.

完全没有必要。相信编译器会转换代码,以便它完成它应该做的所有 逻辑 事情 - 它只运行它的 finally 子句一次,当它用尽迭代器逻辑或当它被显式处理时。


1.NET Reflector 生成的所有代码示例。但是现在它太擅长反编译这些构造了,所以如果你去看看 Silly 方法本身:

[IteratorStateMachine(typeof(<Silly>d__1)), Extension]
private static IEnumerable<T> Silly<T>(this IEnumerable<T> source)
{
    IEnumerator<T> <sillier>5__1;
    using (<sillier>5__1 = source.GetEnumerator())
    {
        while (<sillier>5__1.MoveNext())
        {
            yield return <sillier>5__1.Current;
        }
    }
    <sillier>5__1 = null;
}

它再次设法隐藏了有关该状态机的大部分细节。您需要追踪 IteratorStateMachine 属性引用的类型才能看到上面显示的所有细节。


2另请注意,编译器没有义务生成状态机以允许迭代器工作。它是当前 C# 编译器的实现细节。 C# 规范对编译器如何 转换迭代器没有任何限制,只是限制效果应该是什么。