【问题标题】:Infinite state machine with an IDisposable具有 IDisposable 的无限状态机
【发布时间】:2016-10-17 14:57:03
【问题描述】:

假设我有一个无限状态机来生成随机 md5 哈希:

public static IEnumerable<string> GetHashes()
{
    using (var hash = System.Security.Cryptography.MD5.Create())
    {
        while (true)
            yield return hash.ComputeHash(Guid.NewGuid().ToByteArray());
    }
}

在上面的示例中,我使用了using 语句。 .Dispose() 方法会被调用吗? CQ,非托管资源会被释放吗?

例如,如果我使用机器如下:

public static void Test()
{
    int counter = 0;
    var hashes = GetHashes();
    foreach(var md5 in hashes)
    {
        Console.WriteLine(md5);
        counter++;
        if (counter > 10)
            break;
    }
}

由于hashes 变量将超出范围(我假设垃圾已收集),是否会调用 dispose 方法来释放 System.Security.Cryptography.MD5 使用的资源,或者这是内存泄漏?

【问题讨论】:

  • 用一些先编译的代码试试。就像,实际上有一个yield return 声明会有所帮助。 :-)
  • 对您的问题的简短回答是“是的,Dispose 将被调用”——您可以通过将MD5.Create 替换为一个在处理时向控制台打印一些内容的类来轻松测试这一点.长答案更有趣,因为它涉及解释迭代器方法编译成什么(以及foreach 在处理迭代器的幕后做了什么)。如果以后没人写我会自己写,但这是 Stack Overflow,所以......机会不大。
  • @JeroenMostert 我的错,只是在我脑海中快速写下。
  • @JeroenMostert 也是,太棒了,我很想知道长篇大论,但至少我知道不会有内存泄漏。 :)
  • 小心术语:你有一个有限状态机!它可以无限期地运行,但状态的数量是非常有限的,这对于它的实现是相当重要的(尽管它巧妙地隐藏在 C# 的 yield return 语法之间。

标签: c# idisposable yield-return


【解决方案1】:

让我们稍微修改一下您的原始代码块,将其归结为基本要素,同时仍保持其足够有趣以供分析。这并不完全等同于您发布的内容,但我们仍在使用迭代器的值。

class Disposable : IDisposable {
    public void Dispose() {
        Console.WriteLine("Disposed!");
    }
}

IEnumerable<int> CreateEnumerable() {
    int i = 0;
    using (var d = new Disposable()) {
       while (true) yield return ++i;
    }
}

void UseEnumerable() {
    foreach (int i in CreateEnumerable()) {
        Console.WriteLine(i);
        if (i == 10) break;
    }
}

这将在打印Disposed!之前打印从1到10的数字

幕后究竟发生了什么?还有很多。让我们先处理外层,UseEnumerableforeach 是以下语法糖:

var e = CreateEnumerable().GetEnumerator();
try {
    while (e.MoveNext()) {
        int i = e.Current;
        Console.WriteLine(i);
        if (i == 10) break;
    }
} finally {
    e.Dispose();
}

对于确切的细节(因为即使这是简化了一点)我推荐你the C# language specification,第 8.8.4 节。这里的重要一点是foreach 需要隐式调用枚举器的Dispose

接下来,CreateEnumerable 中的 using 语句也是语法糖。事实上,让我们把整个事情写成原始语句,这样我们以后可以更清楚地理解翻译:

IEnumerable<int> CreateEnumerable() {
    int i = 0;
    Disposable d = new Disposable();
    try {
       repeat: 
       i = i + 1;
       yield return i;
       goto repeat;
    } finally {
       d.Dispose();
    }
}

实现迭代器块的确切规则在语言规范的第 10.14 节中有详细说明。它们是根据抽象操作而不是代码给出的。 C# in Depth 中给出了关于 C# 编译器生成什么样的代码以及每个部分的作用的很好的讨论,但我将给出一个仍然符合规范的简单翻译。重申一下,这不是编译器实际上会产生的,但它是一个足够好的近似值来说明正在发生的事情,并省略了处理线程和优化的更多毛茸茸的部分。

class CreateEnumerable_Enumerator : IEnumerator<int> {
    // local variables are promoted to instance fields
    private int i;
    private Disposable d;

    // implementation of Current
    private int current;
    public int Current => current;
    object IEnumerator.Current => current;

    // State machine
    enum State { Before, Running, Suspended, After };
    private State state = State.Before;

    // Section 10.14.4.1
    public bool MoveNext() {
        switch (state) {
            case State.Before: {
                    state = State.Running;
                    // begin iterator block
                    i = 0;
                    d = new Disposable();
                    i = i + 1;
                    // yield return occurs here
                    current = i;
                    state = State.Suspended;
                    return true;
                }
            case State.Running: return false; // can't happen
            case State.Suspended: {
                    state = State.Running;
                    // goto repeat
                    i = i + 1;
                    // yield return occurs here
                    current = i;
                    state = State.Suspended;
                    return true;
                }
            case State.After: return false; 
            default: return false;  // can't happen
        }
    }

    // Section 10.14.4.3
    public void Dispose() {
        switch (state) {
            case State.Before: state = State.After; break;
            case State.Running: break; // unspecified
            case State.Suspended: {
                    state = State.Running;
                    // finally occurs here
                    d.Dispose();
                    state = State.After;
                }
                break;
            case State.After: return;
            default: return;    // can't happen
        }
    }

    public void Reset() { throw new NotImplementedException(); }
}

class CreateEnumerable_Enumerable : IEnumerable<int> {
  public IEnumerator<int> GetEnumerator() {
    return new CreateEnumerable_Enumerator();
  }

  IEnumerator IEnumerable.GetEnumerator() {
    return GetEnumerator();
  }
}

IEnumerable<int> CreateEnumerable() {
  return new CreateEnumerable_Enumerable();
}

这里的关键是代码块在出现yield returnyield break 语句时被拆分,迭代器负责在中断时记住“我们在哪里”。正文中的任何finally 块都会延迟到Dispose。代码中的无限循环实际上不再是无限循环,因为它被周期性的yield return 语句中断。请注意,因为finally 块实际上不再是finally 块,当您处理迭代器时,它的执行不太确定。这就是为什么使用foreach(或确保在finally 块中调用迭代器的Dispose 方法的任何其他方式)是必不可少的原因。

这是一个简化的例子;当您使循环更复杂、引入异常等时,事情会变得更加有趣。 “只是让这项工作”的负担由编译器承担。

【讨论】:

  • 很棒的答案,正如承诺的那样接受了答案和+1
【解决方案2】:

很大程度上,这取决于您如何编码。但在您的示例中,Dispose 将被调用。

这是explanation on how iterators get compiled

具体来说,谈论finally

迭代器带来了一个尴尬的问题。不是在弹出堆栈帧之前执行整个方法,而是在每次产生值时有效地暂停执行。无法保证调用者会以任何方式、形状或形式再次使用迭代器。如果您需要在产生值后的某个时间点执行更多代码,那么您就有麻烦了:您不能保证它会发生。言归正传,finally 块中的代码通常在几乎所有情况下都会在离开该方法之前执行,因此不能完全依赖它。

...

状态机的构建是为了在正确使用迭代器时执行 finally 块。这是因为 IEnumerator 实现了 IDisposable,并且 C# foreach 循环在迭代器上调用 Dispose(即使是非泛型的 IEnumerator,如果它们实现了 IDisposable)。生成的迭代器中的 IDisposable 实现会计算出哪些 finally 块与当前位置相关(一如既往地基于状态)并执行相应的代码。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-08-10
    相关资源
    最近更新 更多