In fact, the yield
statement is a syntactic sugar that makes the compiler actually generate a class which implements the IEnumerator<T>
interface and rewrites the body of the method using the yield
statement into a state machine.
Each state is associated with a portion of code that ultimately generates the next element in the sequence. This is embedded in the MoveNext()
method. A state machine can represent all necessary constructs (a sequence, a selection, an iteration) and hence all C# code (meaning statements within a method) can be rewritten like that. That is the underlying 'magic' of yield
.
In your specific case, such rewriting into a state machine and the corresponding full implementation of IEnumerator<T>
(and its inherited IEnumerator
(non-generic) and IDisposable
interfaces) will look like this:
public class CustomEnumerator : IEnumerator<int>
{
public int Current { get; private set; }
object IEnumerator.Current => this.Current;
// internal 'position' in the sequence, i.e. the current state of the state machine
private int position = 0;
public bool MoveNext()
{
// advance to next state
// (works for linear algorithms; an alternative is to select the next state at the end of processing the current state)
position++;
// perform the code associated with the current state and produce an element
switch (position)
{
// state 1: line 'yield return 1;'
case 1:
Current = 1;
return true;
// state 2: lines 'Console.WriteLine("1");' and 'yield return 2;'
case 2:
Console.WriteLine("1"); // see also note at the end of this answer
Current = 2;
return true;
// there are no other states in this state machine
default:
return false;
}
}
public void Reset()
{
position = 0;
}
public void Dispose()
{
// nothing to do here
}
}
Each call to MoveNext()
, which is what happens internally in each iteration of a foreach
statement, causes a portion of the code to be executed—until the next element in the sequence is produced.
For this implementation to be usable, a corresponding IEnumerable<T>
implementation is necessary, which is pretty trivial:
public class CustomEnumerable : IEnumerable<int>
{
public IEnumerator<int> GetEnumerator()
{
return new CustomEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}
Then the following two foreach
loops will produce the exact same results:
void Main()
{
// custom implementation of IEnumerator<T>
foreach (int i in new CustomEnumerable())
{
Console.WriteLine(i);
}
// your original implementation—will produce same results
// note: I assume someObject implements IEnumerable<T> and hence your GetEnumerator() method
foreach (int i in someObject)
{
Console.WriteLine(i);
}
}
Note: In your GetEnumerator()
code the call to Console.WriteLine("1");
is after the enumerator returns 1
(and the caller processes it) so that looks a little bit odd.