调用者无法“完成”的'yield'枚举 - 会发生什么

时间:2016-06-09 20:00:42

标签: c# yield-return

假设我有

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())

4 个答案:

答案 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来分析您的案例中生成的代码。它会变得相当复杂,但它会给你一个想法。