yield places values into an IEnumerable
object. To further understand what yield does, here’s an example with trace
statements between each yield statement:
GetValues( )’s yield
statements return hard-coded integer values. Main( ) iterates these
values, printing each. Notice how GetValue( )’s trace statements
interlace with Main( )’s. A yield statement comes in two different
forms, yield return and yield break. yield return places the evaluated
expression as the current value of the IEnumerable. yield break marks the
end of an iterator:
yield break prevents remaining code from executing.
It reports back to the foreach that
there are no values remaining in the IEnumerable. C#’s compiler reports a
warning about any unreachable code following yield break.
Exercise 1:
Use Visual Studio’s debugger to step through (F11) SimpleYield.cs.
Follow the code path as the foreach executes.
Exercise 2:
Create a GetValue( ) method that returns a random number of integer
values. Use yield return and yield break.
yield Benefits
One strong feature of yield is its ability to defer processing. It delays
any calculation until absolutely necessary. Deferred processing makes
more responsive programs. yield spreads wait time amongst all iterations
by creating values only when necessary:
The above example emulates an algorithm that takes a long time to process by
using Thread.Sleep( ) before adding each value. Both foreachs over
CalculateAtOnce( ) and DefferedCalculate( ) print the same results
to the console. However the execution behavior is noticeably
different. CalculateAtOnce( )’s foreach waits for 10 seconds and
prints 0 through 9. DeferredCalculate( )’s foreach prints each
value per second. Another benefit of yield’s deferred processing
is that it does not require a collection stored in memory. yield can
iterate over a large set of data without consuming large amounts of
memory. Take the following example. You iterate over a set of data
with 10 million integer values. Without yield, the entire collection
of values must be returned. With yield, each integer value is
returned when requested.
Start the Windows Task Manager
(Ctrl+Shift+Esc) before running this example. Find your process,
and notice the amount of memory usage from using
DeferredCalculate( ). Press enter and see the drastic increase
that CalculateAtOnce( ) makes; intArray’s 10 million in-memory
values are the cause. yield gives potential to handle data sets
of any size without concern for memory limitations.
Exercise 3:
Use yield to iterate over an infinite data set of random integers.
Under the Hood
Many C# keywords involve the
generation of MSIL code, methods, or even classes. yield
is no exception.
Red Gate’s .NET Reflector
shows the generated C# from using the yield statement.
The disassembled assembly of SimpleYield.cs contains an extra class implementing
IEnumerable.
The compiler generates a switch
statement to handle executing the code written in
your original yielding method. Only one case
statement is used for each time MoveNext( ) is
called. This is how the deferred processing
actually occurs. GetValues( ) is also changed to
use the GeneratedClass( ) in place of the original code.
The -2 in
the new GeneratedClass( ) call is used to set
the initial state. The state’s value is
changed when MoveNext( ) is called. Beginning
each case, state is set to -1 to prevent further
execution if an exception occurs. Just before a
case returns, state is assigned the value of the
next case.
Exercise 4:
Write an implementation of yield’s generated code and execute a foreach over it.