lines of code are not executed after calling method containing yield

老子叫甜甜 提交于 2019-12-31 04:00:13

问题


Consider the following method:

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

Now, I want to call it:

 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...");

But the last line of code never runs. Why?


回答1:


The line yield break; never runs because of the way interators work.

The iterator method GetTimes(int count) is not executed when you do

var times = GetTimes(2);

Rather, it is executed whenever you are pulling a value from it (for example, when you do times.Take(1).Single().ToString()).

There are two things that produce this seemingly weird behavior here:

  1. Iterators stop whenever a yield return line is hit. The iterator execution resumes from the point where it left when you try to get another element from it. If you don't it will never resume execution.

  2. You are actually executing the iterator twice. You do something that's usually referred to as "multiple enumeration of IEnumerable"

To illustrate what actually happens behind the scenes, let's to a small change to your GetTimes method: let's not return the same date every time, but each time it's called we'll return the previous date + 1 day. Let's also add some Console.WriteLine to trace execution. So, the new method body could look like this:

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;
}

Running your code now produces the following output:

returning the value with index 0
First element:11/16/2012 11:34:46 PM
returning the value with index 0
returning the value with index 1
Second element:11/17/2012 11:34:46 PM
Finished...

This illustrates the two points above:

  1. GetTimes execution is stopped as soon as a value is returned and it is resumed from the same state as soon as another one is requested.

  2. The Iterator is executed twice (the second time you use times the Skip requests a value and the Take requests the next value and uses it).

OK, but why doesn't the yield break; get executed? That's because your iterator can produce 2 values and it's called only 2 times, making it freeze after the second yield return is hit. If you were to request the third element from the iterator, then your break line would be hit.

Now, to illustrate a scenario in which the last line gets hit, let's use the enumerator in a usual way (using a foreach loop). Replace your Console.WriteLine lines with:

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

This code will produce the following output:

returning the value with index 0
11/17/2012 12:05:20 AM
returning the value with index 1
11/18/2012 12:05:20 AM
About to hit the `yield break`! Awesome!

As you can see, the foreach consumes the iterator all the way to the end and the yield break line gets hit. You can also confirm this by setting a breakpoint on it.




回答2:


Assuming you mean the last line in the enumerator never runs... the line

yield break;

does not execute because you only take two elements from a sequence that has two elements (with the initial version of your code). The state machine behind the enumerator never gets far enough to execute that line.

The yield break; does indeed run when you try to take 3 elements from the sequence.

I can see no reason (short of possibly an Exception being thrown) why the last line in the calling code should not be called.

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

If that is what you mean, is an Exception being thrown? If so, what is the nature of the Exception?

REWRITE OF PREVIOUS INCORRECT UPDATE

The code as originally written does execute the line

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

It does not execute

yield break;

for the previously stated reasons.

The line that was subsequently added to the question

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

does not succeed, and indeed throws an InvalidOperationException("Sequence contains no elements"), and does run the yield break; line, because you attempt to take 3 elements from a sequence that has only 2.

UPDATE 2

If you're trying to understand how iterator blocks and the Yield keyword work, I strongly suggest the series of blog posts by Eric Lippert (Principal Developer on the Visual C# team) beginning with

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




回答3:


When you try to take "Third element" (out of two in the sequence), an exception will be thrown because the .Single() call has nothing to return.

By the way, are you aware of the .ElementAt(int) method? Why don't you use that?



来源:https://stackoverflow.com/questions/13424485/lines-of-code-are-not-executed-after-calling-method-containing-yield

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!