【问题标题】:Is dispose called down to the bottom via yield return?是否通过收益回报将处置降至最低?
【发布时间】:2019-01-04 15:06:51
【问题描述】:

为了简单起见,我举了一些愚蠢的例子。

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) 它自己会被完全消耗掉。

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

我的猜测是,由于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 并移动枚举器一次即可处理所有必需的东西。

【问题讨论】:

  • 是什么让您认为Take 会处理枚举器? TakeIterator(这是 Take 返回的内容)与源代码的作用不大。特别是它不处理任何东西。
  • 为什么不写你自己的IEnumerator,看看当你这样使用它时是否会调用dispose
  • @HimBromBeere 是什么让您认为 take 不会 处理它创建的枚举器?如果没有,那将是其代码中的错误,您应该将其报告给 MS。当然,查看源代码,它永远不会创建未释放的枚举器。
  • @Servy 你是对的。我只是不想进入兔子洞:)
  • 在这种情况下,编译您的示例然后查看编译器通过反编译器将哪些内容推送到枚举器的实现类中是很有启发性的。你可以看到所有的机器,例如处理Dispose 以确保using 的逻辑finally 或显式finally 被调用。

标签: c# ienumerable dispose yield-return


【解决方案1】:

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;
    }
}

所以我们可以看到这里调用了&lt;&gt;m__Finally1(以及由于退出MoveNext 中的while 循环。

还有&lt;&gt;m__Finally1

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

所以,我们可以看到 sillier 已被释放并且我们进入了否定状态,这意味着 MoveNext 不需要做任何特殊工作处理“我们已经被处理的状态”。

所以,

一个想法:可以有一个 if(disposed) yield break;在每次收益回报之后。现在愚蠢的枚举器的 dispose 方法只需要设置 dispose = true 并移动枚举器一次即可处理所有必需的东西。

完全没有必要。相信编译器会转换代码,以便它完成它应该执行的所有 逻辑 事情 - 它只运行它的 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# 规范对如何编译器转换迭代器没有任何限制,只对效果应该是什么进行限制。

【讨论】:

  • 非常感谢您的解释。所以它是编译器将理解的finally块......所以如果我在finally块之外调用一个不会被调用的dispose方法。 finally 块太棒了!
猜你喜欢
  • 1970-01-01
  • 2011-05-27
  • 1970-01-01
  • 1970-01-01
  • 2012-01-14
  • 1970-01-01
  • 2018-01-23
  • 2010-12-01
  • 1970-01-01
相关资源
最近更新 更多