【问题标题】:lines of code are not executed after calling method containing yield调用包含yield的方法后不执行代码行
【发布时间】:2012-11-05 15:28:01
【问题描述】:

考虑以下方法:

IEnumerable<DateTime> GetTimes(int count)
{
 for (int i = 0; i < count; i++)
      yield return DateTime.Now;
 yield break;
}

现在,我想叫它:

 var times = GetTimes(2);
 Console.WriteLine("First element:" + times.Take(1).Single().ToString());
 Console.WriteLine("Second element:" + times.Skip(1).Take(1).Single().ToString());
 Console.WriteLine("Third element:" + times.Skip(2).Take(1).Single().ToString());
 Console.WriteLine("Finished...");

但最后一行代码永远不会运行。为什么?

【问题讨论】:

  • @Tigran:感谢您的评论。我添加了这一行来指示变量时间是什么。
  • @Alireza 实际上,如果你执行GetTimes(2).Skip(2).Take(1)yield break; 就会被调用。我想你的意思是在最后一个场景中写GetTimes(3)
  • pastebin.com/SKFRzCp3 这是同样的事情,你希望它进入捕获,但它永远不会。因为迭代器永远不会被调用..非常巧妙的技巧..产量总是扭曲我的梅隆:p

标签: c# yield


【解决方案1】:

由于interators 的工作方式,yield break; 行永远不会运行。

执行时不执行迭代器方法GetTimes(int count)

var times = GetTimes(2);

相反,当您从中提取值时(例如,当您执行times.Take(1).Single().ToString() 时),它就会执行。

这里有两件事会产生这种看似奇怪的行为:

  1. 只要遇到yield return 行,迭代器就会停止。当您尝试从中获取另一个元素时,迭代器的执行从它离开的地方开始。如果你不这样做,它将永远不会恢复执行。

  2. 您实际上是在执行迭代器两次。你做的事情通常被称为"multiple enumeration of IEnumerable"

为了说明幕后实际发生的情况,让我们对您的 GetTimes 方法进行一个小改动:我们不要每次都返回相同的日期,但每次调用它时,我们都会返回前一个日期 + 1 天。我们还添加一些Console.WriteLine 来跟踪执行。因此,新方法体可能如下所示:

IEnumerable<DateTime> GetTimes(int count)
{
    for (int i = 0; i < count; i++)
    {
        Console.WriteLine("returning the value with index " + i);
        yield return DateTime.Now.AddDays(i);
    }

    Console.WriteLine("About to hit the `yield break`! Awesome!");
    yield break;
}

现在运行您的代码会产生以下输出:

返回索引为 0 的值 第一个元素:2012 年 11 月 16 日晚上 11:34:46 返回索引为 0 的值 返回索引为 1 的值 第二个元素:2012 年 11 月 17 日晚上 11:34:46 完成的...

这说明了以上两点:

  1. GetTimes 执行在返回值后立即停止,并在请求另一个值时从相同状态恢复。

  2. 迭代器执行两次(第二次使用timesSkip 请求一个值,Take 请求下一个值并使用它)。

好的,但是为什么yield break; 没有被执行?那是因为你的迭代器可以产生 2 个值并且它只被调用了 2 次,使它在第二个 yield return 被命中后冻结。 如果您要从迭代器中请求第三个元素,那么您的 break 行将被命中。

现在,为了说明最后一行被命中的场景,让我们以通常的方式使用枚举器(使用foreach 循环)。将您的 Console.WriteLine 行替换为:

foreach (var dateTime in times)
    Console.WriteLine(dateTime);

此代码将产生以下输出:

返回索引为 0 的值 2012 年 11 月 17 日上午 12:05:20 返回索引为 1 的值 2012 年 11 月 18 日上午 12:05:20 即将达到“收益突破”!惊人的!

如您所见,foreach 一直消耗迭代器到最后,yield break 行被命中。您也可以通过在其上设置断点来确认这一点。

【讨论】:

  • 有趣的是,您实际上是在发布输出时意外输出生成的嵌套类型的字符串表示两次而不是输出foreach 变量:-)
  • @JeppeStigNielsen 哇,这个错误真的溜走了。感谢您指出!固定。
【解决方案2】:

假设您的意思是枚举器中的最后一行 永远不会运行...该行

yield break;

不会执行,因为您只从具有两个元素的序列中获取两个元素(使用您的代码的初始版本)。枚举器后面的状态机永远无法执行该行。

当您尝试从序列中获取 3 个元素时,yield break; 确实会运行。

我看不出为什么不应该调用调用代码中的最后一行(可能没有抛出异常)。

Console.WriteLine("Finished...");

如果这就是你的意思,是抛出异常吗?如果是,异常的性质是什么?

重写以前的错误更新

最初编写的代码确实执行该行

Console.WriteLine("Finished...");

它不执行

yield break;

出于前面所述的原因。

随后添加到问题中的行

Console.WriteLine("Third element:" + times.Skip(2).Take(1).Single().ToString());

没有成功,并且确实抛出了 InvalidOperationException("Sequence contains no elements"),并且确实运行了 yield break; 行,因为您尝试从只有 2 个元素的序列中获取 3 个元素。

更新 2

如果您想了解迭代器块和 Yield 关键字的工作原理,我强烈建议您阅读 Eric Lippert(Visual C# 团队的首席开发人员)的系列博文,以

开头

http://blogs.msdn.com/b/ericlippert/archive/2009/07/09/iterator-blocks-part-one.aspx

【讨论】:

  • 感谢您的回答。但不是!没有例外,也没有其他信号表明有任何问题。很奇怪。
  • @Alireza:您从仅返回两个元素的枚举器中获取 3 个元素。枚举器不会在第一个和第二个 Console.WriteLine 之间“倒回”。
  • @ Eric J. 没错。抛出异常选项已关闭,我无法获得异常。非常感谢。
  • @EricJ。为什么枚举器不会像您所说的那样“倒带”?不要在上面的每个代码行中再次调用GetEnumerator()。我想说它就像上面每个代码行中的一个全新的foreach。但是当然没有第三个元素。
  • @EricJ。 yield break; 确实是多余的,但是如果客户端代码从迭代器请求一个更多值,它被调用。
【解决方案3】:

当您尝试获取“第三个元素”(序列中的两个元素)时,将引发异常,因为 .Single() 调用没有可返回的内容。

顺便问一下,你知道.ElementAt(int) 方法吗?你为什么不用那个?

【讨论】:

  • 是的,我知道 ElementAt(i)。但我只是想了解“收益”是如何工作的。
猜你喜欢
  • 1970-01-01
  • 2022-11-27
  • 1970-01-01
  • 2015-03-26
  • 1970-01-01
  • 2022-11-03
  • 1970-01-01
  • 2012-08-11
  • 2017-11-10
相关资源
最近更新 更多