假设我有
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)。因此,我必须在某个地方设置一个可以做一些时髦的事情(明确地获取一个枚举器而不是处理它等)。我会让他们开枪
错误代码
我找到了来电者
IEnumerator<T> enumerator = foo().GetEnumerator();
更改为
using(IEnumerator<T> enumerator = foo().GetEnumerator())
答案 0 :(得分:37)
我刚刚离开了连接。
不,你不是。
什么时候最终被调用?
当放置IEnumerator<T>
时,在获取序列的第一项后将会First
执行(就像每个人在使用IEnumerator<T>
时应该做的那样)。< / p>
现在如果有人写道:
//note no `using` block on `iterator`
var iterator = Foo().GetEnumerator();
iterator.MoveNext();
var first = iterator.Current;
//note no disposal of iterator
然后他们会泄漏资源,但是错误在调用者代码中,而不是迭代器块。
答案 1 :(得分:27)
你最终不会泄露连接。 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
本身。
答案 2 :(得分:22)
好的,这个问题可以使用一些经验数据。
使用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();
产生以下调试输出
1
1
2
2
3
3
Finally
Disposed
1
Finally
Disposed
我们可以看到,它执行finally
块和Dispose
方法。
答案 3 :(得分:1)
没有特别的魔力。如果您查看IEnumerator<T>
上的文档,则会发现它继承自IDisposable
。如你所知,foreach
构造是语法糖,它由编译器分解为枚举器上的一系列操作,整个事物被包装到try
/ finally
块中,在枚举器对象上调用Dispose
。
当编译器将迭代器方法(即包含yield
语句的方法)转换为IEnumerable<T>
/ IEnumerator<T>
的实现时,它会处理try
/ {{1}生成的类的finally
方法中的逻辑。
您可能会尝试使用ILDASM来分析您的案例中生成的代码。它会变得相当复杂,但它会给你一个想法。