问题
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:
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.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:
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.The Iterator is executed twice (the second time you use
times
theSkip
requests a value and theTake
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