具有 IDisposable 的无限状态机

Syn*_*der 4 c# idisposable yield-return

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

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

在上面的例子中我使用了一个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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

Jer*_*ert 5

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

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

幕后到底发生了什么?还有很多。让我们先处理外层UseEnumerable。这foreach是以下语法糖:

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

对于确切的细节(因为即使这也被简化了一点),我建议您参考C# 语言规范,第 8.8.4 节。这里重要的一点是 a需要对枚举器的foreach隐式调用。Dispose

接下来,using中的语句CreateEnumerable也是语法糖。事实上,让我们用原始语句写出整个内容,以便稍后我们可以更理解翻译:

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

语言规范第 10.14 节详细介绍了迭代器块实现的确切规则。它们是以抽象操作而非代码的形式给出的。关于 C# 编译器生成什么类型​​的代码以及每个部分的作用的很好的讨论在C# in Depth中给出,但我将提供一个仍然符合规范的简单翻译。重申一下,这并不是编译器实际生成的结果,但它是一个足够好的近似值,足以说明正在发生的情况,并且省略了处理线程和优化的更多麻烦部分。

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();
}
Run Code Online (Sandbox Code Playgroud)

这里的要点是代码块在yield returnoryield break语句出现时被分割,迭代器负责记住中断时“我们在哪里”。finally正文中的任何块都会推迟到Dispose. 代码中的无限循环实际上不再是无限循环,因为它被周期性yield return语句中断。请注意,因为finally块实际上不再是一个finally块,所以当您处理迭代器时,它的执行不太确定。这就是为什么 using (或确保在块中调用迭代器的方法的foreach任何其他方式)至关重要的原因。Disposefinally

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