【问题标题】:'yield' enumerations that don't get 'finished' by caller - what happens调用者没有“完成”的“yield”枚举 - 会发生什么
【发布时间】:2016-10-10 15:33:37
【问题描述】:

假设我有

IEnumerable<string> Foo()
{
     try
     {

         /// open a network connection, start reading packets
         while(moredata)
         {
            yield return packet; 
        }
     }
     finally
      {
        // close connection 
      }
}

(或者也许我做了一个“使用”——同样的事情)。如果我的来电者离开会发生什么

var packet = Foo().First();

我只剩下一个泄露的连接。 finally 什么时候被调用?还是说正确的事总是靠魔法发生的?

用答案和想法编辑

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

错误代码

我发现来电者在做

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

改为

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

【问题讨论】:

  • 我的最佳猜测:只返回第一个 packet(?)
  • @MaciejLos 这没有回答他提出的问题。
  • @Servy,我知道。这就是我发表评论的原因。如您所见,我在语句末尾添加了?,因为我不确定。感谢您的评论。

标签: c# yield-return


【解决方案1】:

我只剩下一个泄露的连接。

不,你不是。

finally 什么时候被调用?

IEnumerator&lt;T&gt; 被释放时,First 在得到序列的第一项后会做什么(就像每个人在使用IEnumerator&lt;T&gt; 时应该做的那样)。

现在如果有人写:

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

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

【讨论】:

【解决方案2】:

您不会以泄露的连接告终。 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 本身的错误。

【讨论】:

    【解决方案3】:

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

    使用VS2015和一个scratch项目,我写了如下代码:

    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 方法。

    【讨论】:

    • 如果有疑问,写一个测试程序:-) ty
    • @pm100 添加更多信息:我指定我使用的是 2015 编译器,但我怀疑 C# 规范中的范围规则需要这种行为。我的怀疑是,终止枚举器隐含地表现得就像最后一次收益返回只是一个返回,并且所有正常的返回规则都适用。
    【解决方案4】:

    没有什么特别的魔法。如果您查看IEnumerator&lt;T&gt; 上的文档,您会发现它继承自IDisposable。如您所知,foreach 构造是语法糖,编译器将其分解为枚举数上的一系列操作,然后将整个内容包装到 try/finally 块中,调用 Dispose在枚举器对象上。

    当编译器将迭代器方法(即包含yield语句的方法)转换为IEnumerable&lt;T&gt;/IEnumerator&lt;T&gt;的实现时,它会在DisposeDispose方法中处理try/finally逻辑生成的类。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2023-04-01
      • 1970-01-01
      • 2013-10-19
      • 1970-01-01
      • 1970-01-01
      • 2014-05-22
      • 2014-01-25
      • 1970-01-01
      相关资源
      最近更新 更多