Introduction
I am developing an unfinished linear algebra (matrix) class and wanted to provide the ability to enumerate it horizontally (by row) or vertically (by column). So I tried to implement IEnumerable
, this requires implementing a method called GetEnumerator()
. Then it can get a little confusing.
- Creating a second class that implements
IEnumerator
and returning a new instance of it from the GetEnumerator()
method isn't that hard
- Creating more than one isn't any more difficult (vertical and horizontal) and choosing between them using a property is straightforward
- But what about
yield
, how exactly do you use it? Is is 'better'? Do I use it in the matrix class or the IEnumerator
class? - Can you use
yield
and still have more than one way of enumerating?
I tend to find that articles on this subject (and many others) start simple enough and then get more complex / theoretical / detailed than I would like. I would prefer basic, practical advice and guidance with a smattering of theory and explanation, I can learn bit by bit, not all at once. I am not Sheldon. In fact I wasn't completely sure until I'd finished writing the attached demo app, then I understood it (on a practical basis). So, we will look at implementing IEnumerable
, by creating IEnumerator
classes or by the use of yield
- and we will compare the two approaches. I will also mention at least one other way of making a class enumerable (without explicitly implementing IEnumerable
). We will also see two ways of allowing a user of a class to choose from n
different ways of enumerating that class (as many as you implement), one with yield
and one with IEnumerators
.
If you are simply looking to understand (as I was) if yield
is an alternative to creating IEnumerator
classes (rather than something complementary for example) then the answer is: yes; although there are differences which are probably not trivial in many cases, we'll have a short look at this.
What is Yield?
If you arrived here trying to work out what yield
is / does then here's my take.
When you use an Iterator
method with yield
it doesn't execute to its natural conclusion, every time it encounters a yield
it does the same as a return
statement and then stops right where it is, the method doesn't finish, then next time you call the method, it picks up where it left off, i.e. it does not start afresh each time you call it. So during a foreach
loop these methods will run from start to finish once only, not once for each value in a collection, array or other grouping of data. It's a method that returns values many times not once - magic (or perhaps not magic, just the compiler saving us time since behind the scenes(^) it apparently creates IEnumerator
like code anyway...)
Who is it for?
- Beginners
- Anyone who hasn't implemented
IEnumerable
, (via an IEnumerator
or yield
) before and wants an example - Anyone who wants to implement more than one enumerator for an
IEnumerable
class - Anyone who hasn't managed to get their head around one of the million other explanations out there; this is just my addition to that collection and I hope you find it more digestable than the others you've read so far, if not then no shakes, you have 999,999 other options!
Quick Summary
- We can use
yield
as an alternative to creating an IEnumerator
class whilst implementing IEnumerable
(and specifically the GetEnumerator()
method) - In some situations, particularly at moderate to high levels of complexity,
yield
can become orders of magnitude easier to code and understand / read than an IEnumerator
class - A class that is
IEnumerable
can have more than one enumerator, user selectable at run time - You can make a class enumerable without declaring it
IEnumerable
but by implementing a method with a type of IEnumerable
- Implementing more than one method that has an
IEnumerable
return type has the same effect as #3
- When you use
yield
, the compiler creates an IEnumerator
class, so in some ways it's no different, but the code you write may well be easier to read (efficiency of the code is a different question), see the last section
Warning 1: If you like your patterns and principles then even in code this short I have no doubt I've broken many. C'est la vie!
Warning 2: If there is a lack of detail in the semantics, then you have my apologies. I am writing from a practical perspective and sometimes to keep it simple, practical and readable I intentionally (or unintentionally) skip over the detail. For this reason the way two different people interpret the missing detail might be different. If you need or want to get that detail absolutely correct then there is masses of such information out there and I'm not going to try and replicate it here, that's not the intention. If there are clear written errors then I'm very happy to correct them.
Contents
Background
The genesis of this article was (is) an unfinished linear alegbra (matrix) project. I wanted to provide the ability to traverse the matrix by column (vertically) or by row (horizontally) which meant implementing IEnumerable
. OK, so how do we do that then? Well, we start with the classic Google search. A great deal of somewhat frustrating time later I had a good idea I could use the yield
keyword or I could create a class that implemented IEnumerator
, or maybe I had to do both, could do both, didn't have to do either or had to do something else entirely; who knew?
Ultimately you can use yield
or create a class that implements IEnumerator
, your choice. There may be things that each is better suited for, I don't know enough yet to say one way or the other. As for my matrix class I can also implement 2 enumerators (or 10 if I really wanted), each doing something different and which can be interchanged at run time.
The Basics of Enumerating
The 'classic' (or 'old' if you prefer new shiny things) way of implementing IEnumerable
is fairly straight forward so I'll start with a verbal description and then add the code .
The IEnumerable
interface simply demands you implement a GetEnumerator()
method that returns an object that has implemented IEnumerator
. Then IEnumerator
demands you implement several methods, the two key ones are the MoveNext()
method and the Current
property.
So imagine you use a foreach
loop on a object that has implemented IEnumerable
, such as this:
foreach (double element in EnumerableMatrix)
{
Console.WriteLine(element.ToString());
}
What's actually happening is that the GetEnumerator()
method is called on the EnumerableMatrix
object. This returns a new object which we know must have implemented IEnumerator
. This IEnumerator
object can then be looped over using the MoveNext()
method and the Current
property of IEnumerator
. In fact the code above is shorthand for this.
IEnumerator ObjectToEnumerate = EnumerableMatrix.GetEnumerator()
while (while ObjectToEnumerate.MoveNext() == True)
{
Console.WriteLine(ObjectToEnumerate.Current.ToString());
}
You could do this manually in order to gain more control over the enumeration than provided by foreach
. In the Points of Interest section we have a quick look at the IL code and we can see directly that foreach
is turned into MoveNext()
and Current
as shown above.
To Understand Enumeration we Must First Count to Ten
Implements IEnumerable
So, asuming we understand what a foreach
loop actually does then it gets even easier to understand the 'old' or classic way of making an object enumerable. We start with the class we want to enumerate.
public class ByEnumerator : IEnumerable<double>
{
Public IEnumerator<Double> GetEnumerator()
{
Return New MyNewEnumerator()
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}
It really is that straightforward. IEnumerable
simply demands implementation of those two methods and you just point the less specific one at the other.
In my case I was enumerating a two dimensional array of double and wanted to be able to choose between two different IEnumerators
. So we need a few changes. We've done the following:
Added a constructor that simply takes an existing 2 dimensional array of double.
public ByEnumerator(double[,] matrix)
{
this._matrix = matrix;
this._matrixEnumerator = MatrixEnumerator.Horizontal;
}
A property to allow the choice between different enumeration methods and an enum
to represent the different possible enumerators.
public MatrixEnumerator Enumerator
{
get { return this._matrixEnumerator; }
set { this._matrixEnumerator = value; }
}
public enum MatrixEnumerator
{
Vertical,
Horizontal
}
And finally some conditional code to create the desired IEnumerator
instance.
public IEnumerator<double> GetEnumerator()
{
switch (this._matrixEnumerator)
{
case MatrixEnumerator.Vertical:
return new VerticalMatrixEnumerator(this._matrix);
case MatrixEnumerator.Horizontal:
return new HorizontalMatrixEnumerator(this._matrix);
default:
throw new InvalidOperationException();
}
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
Now all we need to do is implement those two new classes, the Horizontal and Vertical enumerators.
Implements IEnumerator
You can see that the IEnumerator
we return depends on the class property, Enumerator
. Now we just need to create two two IEnumerator
classes mentioned in the code above. We'll show the Horizontal one first and you'll see that we simply add code to the four methods mentioned earlier. There are various private fields to keep track of where we are in the array and other than that the important method is MoveNext()
. You can also see that we pass a copy of the 2D array from the IEnumerable
class to the IEnumerator
class (I assume this is why modifiying an object whilst enumerating it tends to cause chaos).
First off the private fields and the constructor.
public class HorizontalMatrixEnumerator : IEnumerator<double>
{
private double[,] _matrix;
private int _colIndex;
private int _rowIndex;
private double _curItem;
private int _lastCol;
private int _lastRow;
public HorizontalMatrixEnumerator(double[,] matrix)
{
this._matrix = matrix;
this._colIndex = -1;
this._rowIndex = 0;
this._curItem = double.NaN;
this._lastCol = matrix.GetUpperBound(1);
this._lastRow = matrix.GetUpperBound(0);
}
- The
_matrix
field is pretty obvious, it's a copy of the 2d array of double sent by the IEnumerator
object (I am deliberately avoiding discussion of shallow / deep copies and values / references - keep the focus)
- The
_colIndex
and _rowIndex
fields keep track of where we are in the array, our position. We'll explain why they are initialised as 0
and -1
in a minute.
-
_curItem
is the field behind the Current
property.
-
_lastCol
and _lastRow
just provide convenient access to the UpperBounds
of the array.
Now the Current
property.
public double Current
{
get
{
if (double.IsNaN(this._curItem))
{
throw new InvalidOperationException();
}
return this._curItem;
}
}
object System.Collections.IEnumerator.Current
{
get { return this.Current(); }
}
As noted, it's just a wrapper for the _curItem
field. It throws an exception if it's NaN
- it is initialised as such in the constructor - this ensures that we throw an exception if the user of the object (the foreach
loop for example) tries to use it before MoveNext()
has been called, it's a bit nicer than letting them find out when their code throws a wobbly because it was expecting a useful value and got something it didn't know how to deal with.
Now the interesting bit, MoveNext()
.
public bool MoveNext()
{
if (this._colIndex == this._lastCol & this._rowIndex == this._lastRow)
{
return false;
}
if (this._colIndex == this._lastCol)
{
this._colIndex = 0;
this._rowIndex += 1;
}
else
{
this._colIndex += 1;
}
this._curItem = this._matrix[this._rowIndex, this._colIndex];
return true;
}
- First off we check we are not already at the end of the array, if we are then we've already finished and we return
false
, allowing the while {true} do ...
loop from earlier to exit gracefully - Then we check if we are at the last column in a row, if we are we reset the column index to
0
and move to the next row - If we weren't at the end of a row then we simply move to the next column in that row
- In the latter two cases we return
true
, telling the while {true} do ...
loop that there's at least one more new value to loop over - Now the explanation I promised earlier, why do we initialise
_rowIndex
as 0
and _colIndex
as -1
? Well, MoveNext()
gets called before Current
, so if we set both to 0
then the code above would immedaitely move us to the second column in the first row (0,1) and we would never set the value of Current
to be the array value at (0,0)
It's worth noting that the MoveNext()
method presented above is essentially a pair of nested for
loops which have been made unnecessarily complex, the yield
version (that we will show shortly) is easier to code and is also easier to understand. The image below should make that clear.
You can see that every time the column index reaches 2
we need to reset it to 0
and add 1
to the row index, or simply add 1
to the column index until is does reach 2
.
Finally the Reset()
method just puts the relevant fields back to how they were when the object was created.
public void Reset()
{
this._colIndex = -1;
this._rowIndex = 0;
this._curItem = double.NaN;
}
}
The Vertical enumerator changes a few things but mostly the MoveNext()
method as shown below.
We just flip around the row and column variables in the if () ... else ...
section such that we move down columns rather than across rows. We also initialise _rowIndex
to -1
(instead of 0
) and _colIndex
to 0
(instead of -1
).
public bool MoveNext()
{
if (this._colIndex == this._lastCol & this._rowIndex == this._lastRow)
{
return false;
}
if (this._rowIndex == this._lastRow)
{
this._rowIndex = 0;
this._colIndex += 1;
}
else
{
this._rowIndex += 1;
}
this._curItem = this._matrix[this._rowIndex, this._colIndex];
return true;
}
Now whilst that's pretty easy, it means creating two extra classes and (relatively) more code than feels necessary for something relatively simple, can we do better? Let's have a crack at yield...
Who Needs IEnumerators?
Yield to temptation. It may not pass your way again
So how do we do the same thing with yield
?
Most of the code doesn't change. The first thing is that the GetEnumerator()
method doesn't create new objects, it calls a couple of private class methods. Other than that it's identical, you have to be watching carefully just to notice that something has changed.
public IEnumerator<double> GetEnumerator()
{
switch (this._matrixEnumerator)
{
case MatrixEnumerator.Horizontal:
return this.HorizontalEnumerator();
case MatrixEnumerator.Vertical:
return this.VerticalEnumerator();
default:
throw new InvalidOperationException();
}
}
Essentially identical, but rather than return new HorizontalMatrixEnumerator(this._matrix)
we see return this.HorizontalEnumerator()
, which is a method reference not an object. Then we just need to implement those two new methods.
private IEnumerator<double> VerticalEnumerator()
{
if (this._matrix != null)
{
for (int col = 0; col <= this._matrix.GetUpperBound(1); col++)
{
for (int row = 0; row <= this._matrix.GetUpperBound(0); row++)
{
yield return this._matrix[row, col];
}
}
} else {
throw new InvalidOperationException();
}
}
private IEnumerator<double> HorizontalEnumerator()
{
if (this._matrix != null)
{
for (int row = 0; row <= this._matrix.GetUpperBound(0); row++)
{
for (int col = 0; col <= this._matrix.GetUpperBound(1); col++)
{
yield return this._matrix[row, col];
}
}
} else {
throw new InvalidOperationException();
}
}
It took me a while to figure out exactly how to declare those two methods but eventually you work out that they need to have a return type of IEnumerator
. You can see this in the code and other than that it involves no more than looping your way through the array. Not much easier technically, in fact no easier at all but certainly less code and without those extra classes to maintain.
What is very clear though is that it's a lot less code than two IEnumerator
classes. It's also a lot easier to see what's going on, it is clearly two nested for
loops, which as we said above is what the MoveNext()
method is actually doing, only in a more complex way.
By way of explanation this really isn't very much, but if you've read and understood how to do the same thing with IEnumerators
then understanding yield
is as simple as this:
- Recognising that the code looks really very similar to the
MoveNext()
methods (in essence), only it's simpler - Realising that when you use an
Iterator
method with yield
it doesn't execute to its natural conclusion, every time it encounters a yield
it does the same as a return
statement and then stops right where it is, the method doesn't finish, then next time you call the method it picks up where it left off, i.e. it does not start afresh each time you call it. So during a foreach
loop this method will run from start to finish once only, not once for each value in the array. It's a method that returns values many times not once - magic (or perhaps not magic, just the compiler saving us time since behind the scenes(^) it apparently creates IEnumerator
like code anyway...)
The only difference between the two is which way you loop through the array, columns first or rows first.
State Machines
If this explanation doesn't work for you there's
of options out there that explain it in different ways and to greater detail, Google(^) is your friend.
OK, I'm not going to try and explain this in great detail (the article is aimed at beginners and there's much I don't yet understand or at least I imagine there is), just enough (I hope) to make sense of what yield
actually does and how. You can almost think of the Iterator
method as a separate programme, the programme running the foreach
loop on your IEnumerable
object then sends a message to the other one saying 'give me the next one' - right now this sounds exactly like a MoveNext()
followed by a Current
- but the clever bit is the other programme stops execution as soon as it has executed yield
statement, then waits until you ask it for the next one, at which point it executes the next statement and continues until it reachs the next yield
statement, executes it and then waits once more. In the case above each time it reaches the 'next' yield
statement it is actually the same one, but in a loop, you could equally have a set of yield
statements one after the other (see the MSDN example(^))
Points of Interest
Did you learn anything interesting / fun / annoying?
Yes, some donkey (when designing .Net and VB / C#) decided that it would be smart to index arrays as (row, column) and DataGridViews as (column, row).
Did you do anything particularly clever or wild or zany?
Wild or zany? This is what the CP article template suggests. Really? When coding? Wild and zany. Maybe if I drank too much coffee and did something really crazy that was in no way related to coding but otherwise, no.
foreach == while (MoveNext()) + Current
We can show clearly the point made earlier, that running foreach on an IEnumerable
object is the same as manually creating the IEnumerator
and initiating a while (true) do ...
loop. This happens to be the code for the foreach
performed on the object which uses an IEnumerator
rather than yield
, although it's very similar indeed for the yield
version.
IL_016d: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<float64> IEnumerableCS.ByEnumerator::GetEnumerator()
IL_0172: stloc.s CS$5$0001
.try
{
IL_0174: br.s IL_01a1
IL_0176: ldloc.s CS$5$0001
IL_0178: callvirt instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1<float64>::get_Current()
IL_017d: conv.r8
IL_017e: stloc.s Val
IL_0180: nop
IL_0181: ldloc.s output
IL_0183: ldloc.s Val
IL_0185: ldc.i4.2
IL_0186: call float64 [mscorlib]System.Math::Round(float64, int32)
IL_018b: stloc.s CS$0$0002
IL_018d: ldloca.s CS$0$0002
IL_018f: call instance string [mscorlib]System.Double::ToString()
IL_0194: ldstr " ¦ "
IL_0199: call string [mscorlib]System.String::Concat(string, string, string)
IL_019e: stloc.s output
IL_01a0: nop
IL_01a1: ldloc.s CS$5$0001
IL_01a3: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
IL_01a8: stloc.s CS$4$0000
IL_01aa: ldloc.s CS$4$0000
IL_01ac: brtrue.s IL_0176
IL_01ae: leave.s IL_01c4
}
finally
{
IL_01b0: ldloc.s CS$5$0001
IL_01b2: ldnull
IL_01b3: ceq
IL_01b5: stloc.s CS$4$0000
IL_01b7: ldloc.s CS$4$0000
IL_01b9: brtrue.s IL_01c3
IL_01bb: ldloc.s CS$5$0001
IL_01bd: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_01c2: nop
IL_01c3: endfinally
You can clearly see that at IL_0174
it jumps (unconditionally) to the short section that includes the call to MoveNext()
, starting at IL_01a1
. At the end of that section it branches on true
(IL_01b9
) to the slightly longer section above (assuming MoveNext()
returned true
), starting at IL_0176
which then calls Current
. Completely consistent with a while (true) do ...
loop, i.e. if the object to enumerate is empty it never tries to call Current
; meanwhile for a non-empty object the first time that MoveNext()
returns false
is when it tries to move past the last position, at which point the branch on true
(IL_01b9
) doesn't branch and the code flows onwards out of the loop.
How Does the Compiler Implement Yield?
I thought I'd remembered reading that when you use yield the compiler responds by creating an IEnumerator
, so it's really just shorthand to allow for code that's easier to read, write and maintain, I thought I'd see if this was true - since I have the perfect examnple to test it on - by looking at what IL code is produced by the compiler. (Try ILSpy(^) or the ILDisassembler that comes with Windows 7.1 SDK(^).) Here's what you see, first we'll look at the classes and code that is produced, in particular the ByYield class.
There's our HorizontalEnumerator
method highlighted, now what does the IL code say?
.method private hidebysig
instance class [mscorlib]System.Collections.Generic.IEnumerator`1<float64> HorizontalEnumerator () cil managed
{
.maxstack 2
.locals init (
[0] class IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'
)
IL_0000: ldc.i4.0
IL_0001: newobj instance void IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::.ctor(int32)
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: ldarg.0
IL_0009: stfld class IEnumerableCS.ByYield IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<>4__this'
IL_000e: ldloc.0
IL_000f: ret
}
I don't get all of that, but I do see a reference to IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4
. We can see that in the image just above - it's the third item in the ByYield
description, just under DerivedTypes
. Here's what's contained in that class member.
There's are old friends, two methods MoveNext()
and Reset()
, and the Current
property; those methods were not written in the ByYield
class, the compiler has created them. Cearly the compiler treats yield
as shorthand for 'please write me an IEnumerator
'.
We can compare, at a very amateur level, whether the compilers version of an IEnumerator
is much better than its attempt to optimise the MoveNext()
we wrote above. The compilers conversion of the two nested for
loops with a yield
in the middle is on the left. It's conversion of my MoveNext()
method is on the right.
.method private final hidebysig newslot virtual
instance bool MoveNext () cil managed
{
.override method instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
.maxstack 4
.locals init (
[0] bool CS$1$0000,
[1] int32 CS$4$0001,
[2] bool CS$4$0002
)
IL_0000: ldarg.0
IL_0001: ldfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<>1__state'
IL_0006: stloc.1
IL_0007: ldloc.1
IL_0008: switch (IL_0019, IL_0017)
IL_0015: br.s IL_001b
IL_0017: br.s IL_007f
IL_0019: br.s IL_0020
IL_001b: br IL_00f2
IL_0020: ldarg.0
IL_0021: ldc.i4.m1
IL_0022: stfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<>1__state'
IL_0027: nop
IL_0028: ldarg.0
IL_0029: ldfld class IEnumerableCS.ByYield IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<>4__this'
IL_002e: ldfld float64[0..., 0...] IEnumerableCS.ByYield::_matrix
IL_0033: ldnull
IL_0034: ceq
IL_0036: stloc.2
IL_0037: ldloc.2
IL_0038: brtrue IL_00ea
IL_003d: nop
IL_003e: ldarg.0
IL_003f: ldc.i4.0
IL_0040: stfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<row>5__5'
IL_0045: br.s IL_00c4
IL_0047: nop
IL_0048: ldarg.0
IL_0049: ldc.i4.0
IL_004a: stfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<col>5__6'
IL_004f: br.s IL_0095
IL_0051: nop
IL_0052: ldarg.0
IL_0053: ldarg.0
IL_0054: ldfld class IEnumerableCS.ByYield IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<>4__this'
IL_0059: ldfld float64[0..., 0...] IEnumerableCS.ByYield::_matrix
IL_005e: ldarg.0
IL_005f: ldfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<row>5__5'
IL_0064: ldarg.0
IL_0065: ldfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<col>5__6'
IL_006a: call instance float64 float64[0..., 0...]::Get(int32, int32)
IL_006f: stfld float64 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<>2__current'
IL_0074: ldarg.0
IL_0075: ldc.i4.1
IL_0076: stfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<>1__state'
IL_007b: ldc.i4.1
IL_007c: stloc.0
IL_007d: br.s IL_00f6
IL_007f: ldarg.0
IL_0080: ldc.i4.m1
IL_0081: stfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<>1__state'
IL_0086: nop
IL_0087: ldarg.0
IL_0088: dup
IL_0089: ldfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<col>5__6'
IL_008e: ldc.i4.1
IL_008f: add
IL_0090: stfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<col>5__6'
IL_0095: ldarg.0
IL_0096: ldfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<col>5__6'
IL_009b: ldarg.0
IL_009c: ldfld class IEnumerableCS.ByYield IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<>4__this'
IL_00a1: ldfld float64[0..., 0...] IEnumerableCS.ByYield::_matrix
IL_00a6: ldc.i4.1
IL_00a7: callvirt instance int32 [mscorlib]System.Array::GetUpperBound(int32)
IL_00ac: cgt
IL_00ae: ldc.i4.0
IL_00af: ceq
IL_00b1: stloc.2
IL_00b2: ldloc.2
IL_00b3: brtrue.s IL_0051
IL_00b5: nop
IL_00b6: ldarg.0
IL_00b7: dup
IL_00b8: ldfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<row>5__5'
IL_00bd: ldc.i4.1
IL_00be: add
IL_00bf: stfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<row>5__5'
IL_00c4: ldarg.0
IL_00c5: ldfld int32 IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<row>5__5'
IL_00ca: ldarg.0
IL_00cb: ldfld class IEnumerableCS.ByYield IEnumerableCS.ByYield/'<HorizontalEnumerator>d__4'::'<>4__this'
IL_00d0: ldfld float64[0..., 0...] IEnumerableCS.ByYield::_matrix
IL_00d5: ldc.i4.0
IL_00d6: callvirt instance int32 [mscorlib]System.Array::GetUpperBound(int32)
IL_00db: cgt
IL_00dd: ldc.i4.0
IL_00de: ceq
IL_00e0: stloc.2
IL_00e1: ldloc.2
IL_00e2: brtrue IL_0047
IL_00e7: nop
IL_00e8: br.s IL_00f1
IL_00ea: nop
IL_00eb: newobj instance void [mscorlib]System.InvalidOperationException::.ctor()
IL_00f0: throw
IL_00f1: nop
IL_00f2: ldc.i4.0
IL_00f3: stloc.0
IL_00f4: br.s IL_00f6
IL_00f6: ldloc.0
IL_00f7: ret
}
.method public final hidebysig newslot virtual
instance bool MoveNext () cil managed
{
.maxstack 4
.locals init (
[0] bool CS$1$0000,
[1] bool CS$4$0001
)
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldfld int32 IEnumerableCS.HorizontalMatrixEnumerator::_colIndex
IL_0007: ldarg.0
IL_0008: ldfld int32 IEnumerableCS.HorizontalMatrixEnumerator::_lastCol
IL_000d: ceq
IL_000f: ldarg.0
IL_0010: ldfld int32 IEnumerableCS.HorizontalMatrixEnumerator::_rowIndex
IL_0015: ldarg.0
IL_0016: ldfld int32 IEnumerableCS.HorizontalMatrixEnumerator::_lastRow
IL_001b: ceq
IL_001d: and
IL_001e: ldc.i4.0
IL_001f: ceq
IL_0021: stloc.1
IL_0022: ldloc.1
IL_0023: brtrue.s IL_002a
IL_0025: nop
IL_0026: ldc.i4.0
IL_0027: stloc.0
IL_0028: br.s IL_0089
IL_002a: ldarg.0
IL_002b: ldfld int32 IEnumerableCS.HorizontalMatrixEnumerator::_colIndex
IL_0030: ldarg.0
IL_0031: ldfld int32 IEnumerableCS.HorizontalMatrixEnumerator::_lastCol
IL_0036: ceq
IL_0038: ldc.i4.0
IL_0039: ceq
IL_003b: stloc.1
IL_003c: ldloc.1
IL_003d: brtrue.s IL_0058
IL_003f: nop
IL_0040: ldarg.0
IL_0041: ldc.i4.0
IL_0042: stfld int32 IEnumerableCS.HorizontalMatrixEnumerator::_colIndex
IL_0047: ldarg.0
IL_0048: dup
IL_0049: ldfld int32 IEnumerableCS.HorizontalMatrixEnumerator::_rowIndex
IL_004e: ldc.i4.1
IL_004f: add
IL_0050: stfld int32 IEnumerableCS.HorizontalMatrixEnumerator::_rowIndex
IL_0055: nop
IL_0056: br.s IL_0068
IL_0058: nop
IL_0059: ldarg.0
IL_005a: dup
IL_005b: ldfld int32 IEnumerableCS.HorizontalMatrixEnumerator::_colIndex
IL_0060: ldc.i4.1
IL_0061: add
IL_0062: stfld int32 IEnumerableCS.HorizontalMatrixEnumerator::_colIndex
IL_0067: nop
IL_0068: ldarg.0
IL_0069: ldarg.0
IL_006a: ldfld float64[0..., 0...] IEnumerableCS.HorizontalMatrixEnumerator::_matrix
IL_006f: ldarg.0
IL_0070: ldfld int32 IEnumerableCS.HorizontalMatrixEnumerator::_rowIndex
IL_0075: ldarg.0
IL_0076: ldfld int32 IEnumerableCS.HorizontalMatrixEnumerator::_colIndex
IL_007b: call instance float64 float64[0..., 0...]::Get(int32, int32)
IL_0080: stfld float64 IEnumerableCS.HorizontalMatrixEnumerator::_curItem
IL_0085: ldc.i4.1
IL_0086: stloc.0
IL_0087: br.s IL_0089
IL_0089: ldloc.0
IL_008a: ret
}
I'm not qualified to comment with any professionalism, but whilst the compiler clearly has to do more work with yield
in order to turn two nested for
loops into an IEnumerator
than when it is given dedicated IEnumerator
code, there isn't (seemingly) a huge difference between the two. Whether one of them is more efficient I don't know - maybe someone has time to test... I guess it shouldn't be a surprise, the fact that the compiler is capable of doing this at all impresses me, especially if you imagine just how complex some methods with a yield
statement might be and it has to be able to handle tham all.
It's probably worth thinking about this is if you have a class that may contain very large amounts of data, which needs to be enumerable and where time is critical, in such cases trying both your own IEnumerator
and yield
is probably worth the time, to see which results in quicker execution.
History
Version 5 (21 April 2013): Fixed issue with test for var == double.NaN
, should have been double.IsNaN(var)
; added section on 'foreach == while (MoveNext()) + Current' further typographical errors fixed and re-wrote some sections for clarity
Version 4 (13 April 2013): [No code changes in demo app] Re-wrote introduction to improve flow, fixed some typographical errors
Version 3 (12 April 2013): [No code changes in demo app] Changed do while to while do
Version 2 (11 April 2013): [No code changes in demo app] Clarifications and extra information subsequent to reader comments and addition of new section, 'How Does the Compiler Implement Yield?'
Version 1 (7 April 2013): Shiny and Newny and New