How yield
implements the pattern of lazy loading
?
views:
206answers:
3yield implementation doesn't reach the code until it's needed.
For example, this code:
public IEnumerable<int> GetInts()
{
yield return 1;
yield return 2;
yield return 3;
}
Will actually compile into a nested class which implements IEnumerable<int>
and the body of GetInts()
will return an instance of that class.
Using reflector you can see:
public IEnumerable<int> GetInts()
{
<GetInts>d__6d d__d = new <GetInts>d__6d(-2);
d__d.<>4__this = this;
return d__d;
}
Edit - adding more info about GetInts
implementation:
The way this implementation makes it lazy is based on the Enumerator
MoveNext()
method. When the enumerable nested class is generated (<GetInts>d__6d
in the example), it has a state and to each state a value is connected (this is a simple case, in more advanced cases the value will be evaluated when the code reach the state). If we take a look in the MoveNext()
code of <GetInts>d__6d
we'll see the state:
private bool MoveNext()
{
switch (this.<>1__state)
{
case 0:
this.<>1__state = -1;
this.<>2__current = 1;
this.<>1__state = 1;
return true;
case 1:
this.<>1__state = -1;
this.<>2__current = 2;
this.<>1__state = 2;
return true;
case 2:
this.<>1__state = -1;
this.<>2__current = 3;
this.<>1__state = 3;
return true;
case 3:
this.<>1__state = -1;
break;
}
return false;
}
When the enumerator is asked for the current object it returns the object which is connected to the current state.
In order to show that the code is evaluated only when it's required you can look at this example:
[TestFixture]
public class YieldExample
{
private int flag = 0;
public IEnumerable<int> GetInts()
{
yield return 1;
flag = 1;
yield return 2;
flag = 2;
yield return 3;
flag = 3;
}
[Test]
public void Test()
{
int expectedFlag = 0;
foreach (var i in GetInts())
{
Assert.That(flag, Is.EqualTo(expectedFlag));
expectedFlag++;
}
Assert.That(flag, Is.EqualTo(expectedFlag));
}
}
I hope it's a bit more clear. I recommend to take a look at the code with Reflector and observe the compiled code as you change the "yield" code.
If you want to know more about what the compiler is doing when using yield return
, check this article by Jon Skeet: Iterator block implementation details
Basically iterators implementated by using yield
statements are compiled into a class that implements a state machine.
If you never foreach
(=iterate over and actually use) the returned IEnumerable<T>
, the code is never actually executed. And if you do, only the minimal code needed to determine the next value to return is executed, only to resume execution when a next value is requested.
You can actually see this behavior happening when you single step such code in the debugger. Try it at least once: I think it's illuminating to see this happen step by step.