Infinite state machine with an IDisposable
假设我有一个无限状态机来生成随机MD5哈希:
1 2 3 4 5 6 7 8 | public static IEnumerable<string> GetHashes() { using (var hash = System.Security.Cryptography.MD5.Create()) { while (true) yield return hash.ComputeHash(Guid.NewGuid().ToByteArray()); } } |
在上面的示例中,我使用了
例如,如果我使用机器如下:
1 2 3 4 5 6 7 8 9 10 11 12 | public static void Test() { int counter = 0; var hashes = GetHashes(); foreach(var md5 in hashes) { Console.WriteLine(md5); counter++; if (counter > 10) break; } } |
由于
让我们稍微改变一下您的原始代码块,将其简化为基本的代码块,同时仍然保持足够有趣的代码块来进行分析。这并不完全等同于您发布的内容,但我们仍在使用迭代器的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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 2 3 4 5 6 7 8 9 10 | 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节。这里重要的一点是,
其次,
1 2 3 4 5 6 7 8 9 10 11 12 | 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编译器生成什么类型的代码以及每个部分所做的工作,我们将进行深入的讨论,但我将给出一个简单的翻译,而不是仍然符合规范的翻译。重申一下,这不是编译器实际产生的结果,但它是一个很好的近似值,足以说明正在发生的事情,并忽略了处理线程和优化的更复杂的部分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | 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(); } |
这里最重要的一点是,代码块在出现
这是一个简化的例子;当您使循环更复杂、引入异常等时,事情会变得更有趣。"只做这个工作"的负担在编译器上。
很大程度上,这取决于您如何编写代码。但在您的示例中,将调用
下面是如何编译迭代器的解释。
具体来说,说到
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.