“yield”枚举不会被调用者“完成” – 会发生什么

假设我有

IEnumerable<string> Foo() { try { /// open a network connection, start reading packets while(moredata) { yield return packet; } } finally { // close connection } } 

(或者,也许我做了一个'使用' – 同样的事情)。 如果我的呼叫者去了会发生什么

 var packet = Foo().First(); 

我只是留下了泄漏的连接。 什么时候终于被调用? 或者正确的事情总是通过魔法发生

编辑答案和想法

我的示例和其他“正常”(foreach,..)调用模式将很好地工作,因为他们处置IEnumerable(实际上是GetEnumerator返回的IEnumerator)。 因此,我必须有一个调用者在做一些奇怪的事情(明确地获得一个枚举器,而不是处理它或类似的东西)。 我会让他们开枪

糟糕的代码

我find了一个来电者

 IEnumerator<T> enumerator = foo().GetEnumerator(); 

变成

 using(IEnumerator<T> enumerator = foo().GetEnumerator()) 

我只是留下了泄漏的连接。

不你不是。

什么时候终于被调用?

IEnumerator<T>被处理时, First要在得到序列的第一个项目之后做什么(就像每个人在使用IEnumerator<T>时都应该这样做)。

现在如果有人写道:

 //note no `using` block on `iterator` var iterator = Foo().GetEnumerator(); iterator.MoveNext(); var first = iterator.Current; //note no disposal of iterator 

那么他们会泄漏资源,但那里的错误是在调用者代码中,而不是迭代器块。

你不会结束泄漏的连接。 yield return生成的迭代器对象是IDisposable ,而LINQ函数会小心地确保正确的处理。

例如, First()实现如下:

 public static TSource First<TSource>(this IEnumerable<TSource> source) { if (source == null) throw Error.ArgumentNull("source"); IList<TSource> list = source as IList<TSource>; if (list != null) { if (list.Count > 0) return list[0]; } else { using (IEnumerator<TSource> e = source.GetEnumerator()) { if (e.MoveNext()) return e.Current; } } throw Error.NoElements(); } 

请注意source.GetEnumerator()的结果如何using 。 这可以确保调用Dispose ,从而确保在finally块中调用您的代码。

foreach循环的迭代也是如此:无论枚举是否完成,代码都确保处理枚举数。

唯一的情况是,当您最终发生泄漏连接时,您自己调用GetEnumerator ,并且不能正确处理它。 但是,这是使用IEnumerable的代码中的错误,而不是IEnumerable本身。

好的这个问题可以使用一点经验数据。

使用VS2015和一个临时项目,我写了下面的代码:

 private IEnumerable<string> Test() { using (TestClass t = new TestClass()) { try { System.Diagnostics.Debug.Print("1"); yield return "1"; System.Diagnostics.Debug.Print("2"); yield return "2"; System.Diagnostics.Debug.Print("3"); yield return "3"; System.Diagnostics.Debug.Print("4"); yield return "4"; } finally { System.Diagnostics.Debug.Print("Finally"); } } } private class TestClass : IDisposable { public void Dispose() { System.Diagnostics.Debug.Print("Disposed"); } } 

然后调用它两种方式:

 foreach (string s in Test()) { System.Diagnostics.Debug.Print(s); if (s == "3") break; } string f = Test().First(); 

其中产生以下debugging输出

 1 1 2 2 3 3 Finally Disposed 1 Finally Disposed 

正如我们所看到的,它执行finally块和Dispose方法。

没有什么特别的魔法。 如果您检查IEnumerator<T>上的文档,您会发现它从IDisposableinheritance。 如你所知, foreach结构是语法糖,它被编译器分解成一个枚举器的操作序列,整个事物被包装成try / finally块,调用Dispose on enumerator对象。

当编译器将迭代器方法(即包含yield语句的方法)转换为IEnumerable<T> / IEnumerator<T> ,它将处理生成的类的Dispose方法中的try / finally逻辑。

您可以尝试使用ILDASM来分析您的案例中生成的代码。 这将是非常复杂的,但它会给你的想法。