Reputation: 1627
In my code i have a method like this
public static IEnumerable<int> GetDiff(int start, int end)
{
while (start < end)
{
yield return start;
start++;
}
yield break; // do we need to call it explicitly?
}
So, the test cases i'm interested in are GetDiff(1, 5)
and GetDiff(5, 1)
. While it's clear what happen in the first case, it's not quite clear how it get finished in the second without yield break;
after loop
Upvotes: 5
Views: 2561
Reputation: 40315
Popped your code in a compiler, built, and reverse engineered back to C#, this came out:
using System.Collections.Generic;
public static IEnumerable<int> GetDiff(int start, int end)
{
while (start < end)
{
yield return start;
start++;
}
}
I used LINQPad 5 and ILSpy for LINQPad.
No yield break there. Alright, alright, that still has sugar... let us translate to C# 1.0. This is how the code looks like:
using System.Collections.Generic;
using System.Runtime.CompilerServices;
[IteratorStateMachine(typeof(<GetDiff>d__1))]
public static IEnumerable<int> GetDiff(int start, int end)
{
<GetDiff>d__1 <GetDiff>d__ = new <GetDiff>d__1(-2);
<GetDiff>d__.<>3__start = start;
<GetDiff>d__.<>3__end = end;
return <GetDiff>d__;
}
It is creating an instance of a hidden anonymous class <GetDiff>d__1
, sets its start
and end
attributes and return it. We will get back to the -2
we pass to its constructor.
The following is the same code above, except it is in IL:
.method public hidebysig static
class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> GetDiff (
int32 start,
int32 end
) cil managed
{
.custom instance void [mscorlib]System.Runtime.CompilerServices.IteratorStateMachineAttribute::.ctor(class [mscorlib]System.Type) = (
01 00 17 55 73 65 72 51 75 65 72 79 2b 3c 47 65
74 44 69 66 66 3e 64 5f 5f 31 00 00
)
// Method begins at RVA 0x2052
// Code size 22 (0x16)
.maxstack 8
IL_0000: ldc.i4.s -2
IL_0002: newobj instance void UserQuery/'<GetDiff>d__1'::.ctor(int32)
IL_0007: dup
IL_0008: ldarg.0
IL_0009: stfld int32 UserQuery/'<GetDiff>d__1'::'<>3__start'
IL_000e: dup
IL_000f: ldarg.1
IL_0010: stfld int32 UserQuery/'<GetDiff>d__1'::'<>3__end'
IL_0015: ret
} // end of method UserQuery::GetDiff
The class <GetDiff>d__1
looks like this:
// <GetDiff>d__1
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
[CompilerGenerated]
private sealed class <GetDiff>d__1 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IDisposable, IEnumerator
{
private int <>1__state;
private int <>2__current;
private int <>l__initialThreadId;
private int start;
public int <>3__start;
private int end;
public int <>3__end;
int IEnumerator<int>.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
object IEnumerator.Current
{
[DebuggerHidden]
get
{
return <>2__current;
}
}
[DebuggerHidden]
public <GetDiff>d__1(int <>1__state)
{
this.<>1__state = <>1__state;
<>l__initialThreadId = Environment.CurrentManagedThreadId;
}
[DebuggerHidden]
void IDisposable.Dispose()
{
}
private bool MoveNext()
{
switch (<>1__state)
{
default:
return false;
case 0:
<>1__state = -1;
break;
case 1:
<>1__state = -1;
start++;
break;
}
if (start < end)
{
<>2__current = start;
<>1__state = 1;
return true;
}
return false;
}
bool IEnumerator.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
return this.MoveNext();
}
[DebuggerHidden]
void IEnumerator.Reset()
{
throw new NotSupportedException();
}
[DebuggerHidden]
IEnumerator<int> IEnumerable<int>.GetEnumerator()
{
<GetDiff>d__1 <GetDiff>d__;
if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId)
{
<>1__state = 0;
<GetDiff>d__ = this;
}
else
{
<GetDiff>d__ = new <GetDiff>d__1(0);
}
<GetDiff>d__.start = <>3__start;
<GetDiff>d__.end = <>3__end;
return <GetDiff>d__;
}
[DebuggerHidden]
IEnumerator IEnumerable.GetEnumerator()
{
return System.Collections.Generic.IEnumerable<System.Int32>.GetEnumerator();
}
}
Let us start at the constructor:
public <GetDiff>d__1(int <>1__state)
{
this.<>1__state = <>1__state;
<>l__initialThreadId = Environment.CurrentManagedThreadId;
}
We are storing in <>1__state
the value we passed. Remember that was a -2
(new <GetDiff>d__1(-2)
). We are also storing the id of the calling thread.
The first thing the client foreach
loop will do is call GetEnumerator
on the retuned IEnumerable<int>
. There is some logic there:
[DebuggerHidden]
IEnumerator<int> IEnumerable<int>.GetEnumerator()
{
<GetDiff>d__1 <GetDiff>d__;
if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId)
{
<>1__state = 0;
<GetDiff>d__ = this;
}
else
{
<GetDiff>d__ = new <GetDiff>d__1(0);
}
<GetDiff>d__.start = <>3__start;
<GetDiff>d__.end = <>3__end;
return <GetDiff>d__;
}
It is checking that the consumer is the same thread and that state has not changed (from the original -2
) if that holds, it returns itself. Otherwise, it returns a clone. That means that if GetEnumerator
is called from another thread or is called by the same thread after iteration has started, the returned IEnumerator<int>
starts from the beginning (as it should).
Also notice that GetEnumerator
changes the state to 0
. That is important.
Now, pay attention to the MoveNext
method. It is an state machine equivalent to your code:
private bool MoveNext()
{
switch (<>1__state)
{
default:
return false;
case 0:
<>1__state = -1;
break;
case 1:
<>1__state = -1;
start++;
break;
}
if (start < end)
{
<>2__current = start;
<>1__state = 1;
return true;
}
return false;
}
On the first call, state is 0
, the code enters the switch
and sets the state to -1
.
After the switch
, the code checks if start < end
. This is the check to see if it will enter your while
loop. If it does not enter, it just returns false
and it is done. If it enters, you yield return start
, so it puts start
in <>2__current
, changes the state to 1
and returns true
. Since it returned true
, the client foreach
reads the current value, does a loop and calls MoveNext
again...
Second time around, it enters the switch since it is on state 1
, then it changes the state to -1
again, and does start++
which was your next line... now your while
loops, meaning that we have to check start < end
, and that is what it does after the switch
. If that conditional is still true
, it will put the new value of start
in <>2__current
, change the state and return true
.
The client foreach
loop will keep consuming the iterator until the conditional does not hold anymore... then MoveNext
returns false
, this tells the foreach
that the iterator is done and the loop ends.
For reference the following code is equivalent to a foreach
loop (source):
{
E e = ((C)(x)).GetEnumerator();
try {
while (e.MoveNext()) {
V v = (V)(T)e.Current;
embedded_statement
}
}
finally {
... // Dispose e
}
}
So, what does yield break;
do? In this case, nothing. yield break;
is used to indicate that the state machine should end (MoveNext
returns false
), however the state machine will end there anyway because it is the end of the method. As consequence, you will only find yield break;
useful (and meaningful) when it is not at the end of the method. For example see Stanislav Molchanovsky's answer.
Furthermore, I would also argue that adding yield break;
does not contribute to maintainability or readability of the code.
Upvotes: 2
Reputation: 1863
No this is not necessary. It will work:
public static IEnumerable<int> GetDiff(int start, int end)
{
while (start < end)
{
yield return start;
start++;
}
// yield break; - It is not necessary. It is like `return` which does not return a value.
}
In this case, the execution of the function will end simply by exiting it.
But you can write like this:
public static IEnumerable<int> GetDiff(int start, int end)
{
while (true)
{
if (start >= end)
yield break;
yield return start;
start++;
}
Console.WriteLine("Finish"); // note that this line will not be executed
}
Upvotes: 11