具有 IDisposable 的无限状态机

Infinite state machine with an IDisposable

假设我有一个无限状态机来生成随机 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 使用的资源,或者这是内存泄漏?

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

这是一个 explanation on how iterators get compiled

具体来说,谈论 finally:

Iterators pose an awkward problem. Instead of the whole method executing before the stack frame is popped, execution effectively pauses each time a value is yielded. There's no way of guaranteeing that the caller will ever use the iterator again, in any way, shape or form. If you require some more code to be executed at some point after the value is yielded, you're in trouble: you can't guarantee it will happen. To cut to the chase, code in a finally block which would normally be executed in almost all circumstances before leaving the method can't be relied on quite as much.

...

The state machine is built so that finally blocks are executed when an iterator is used properly, however. That's because IEnumerator implements IDisposable, and the C# foreach loop calls Dispose on iterators (even the nongeneric IEnumerator ones, if they implement IDisposable). The IDisposable implementation in the generated iterator works out which finally blocks are relevant to the current position (based on the state, as always) and execute the appropriate code.

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

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

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

背后到底发生了什么?还有更多。让我们先解决外层,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 语句出现时被拆分,迭代器负责在出现时记住 "where we were"中断。正文中的任何 finally 个块都会延迟到 Dispose。您代码中的无限循环实际上不再是无限循环,因为它被周期性的 yield return 语句中断。请注意, 因为 finally 块实际上不再是 finally 块,所以当您处理迭代器时,它的执行不太确定。这就是为什么使用 foreach(或确保在 finally 块中调用迭代器的 Dispose 方法的任何其他方式)是必不可少的。

这是一个简化的例子;当您使循环更复杂、引入异常等时,事情会变得更有趣。 "just making this work" 的负担在编译器身上。