“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>
上的文档,您会发现它从IDisposable
inheritance。 如你所知, foreach
结构是语法糖,它被编译器分解成一个枚举器的操作序列,整个事物被包装成try
/ finally
块,调用Dispose
on enumerator对象。
当编译器将迭代器方法(即包含yield
语句的方法)转换为IEnumerable<T>
/ IEnumerator<T>
,它将处理生成的类的Dispose
方法中的try
/ finally
逻辑。
您可以尝试使用ILDASM来分析您的案例中生成的代码。 这将是非常复杂的,但它会给你的想法。