具有IDisposable的无限状态机

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

public static IEnumerable 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使用的资源,或者这是内存泄漏?

让我们稍微改变你原来的代码块,把它归结为基本要素,同时保持足够有趣的分析。 这与您发布的内容并不完全相同,但我们仍在使用迭代器的值。

 class Disposable : IDisposable { public void Dispose() { Console.WriteLine("Disposed!"); } } IEnumerable 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的数字Disposed!

封面下究竟发生了什么? 还有更多。 让我们首先解决外层, UseEnumerableforeach是以下的语法糖:

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

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

接下来, CreateEnumerableusing语句也是语法糖。 实际上,让我们在原始语句中写出整个内容,以便我们以后可以更好地理解翻译:

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

迭代器块实现的确切规则详见语言规范的第10.14节。 它们是在抽象操作方面给出的,而不是代码。 关于C#编译器生成什么类型​​的代码以及每个部分所执行的操作的详细讨论都是在深度的C#中给出的,但是我将提供一个简单的转换,但仍然符合规范。 重申一下,这不是编译器实际生成的内容,但它足以说明正在发生的事情,并且遗漏了处理线程和优化的更多毛茸茸的位。

 class CreateEnumerable_Enumerator : IEnumerator { // 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 { public IEnumerator GetEnumerator() { return new CreateEnumerable_Enumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } IEnumerable CreateEnumerable() { return new CreateEnumerable_Enumerable(); } 

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

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

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

这是关于如何编译迭代器的解释 。

特别是, finally谈到:

迭代器构成了一个尴尬的问题。 不是在弹出堆栈帧之前执行整个方法,而是每次产生值时执行都会有效地暂停。 无法保证调用者将以任何方式,形状或forms再次使用迭代器。 如果你需要在产生价值后的某个时刻执行更多的代码,那么你就会遇到麻烦:你不能保证它会发生。 为了切入追逐,在离开方法之前通常在几乎所有情况下都会执行的finally块中的代码不能完全依赖。

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