Introduction
This is a VB version of the C# article, I decided there were substantial enough differences between the code and some of the text to warrant posting this as an alternative to the other since I felt it would become too long and confusing to try and merge the two.
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 For Each
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 (though they tend to be C# rather than VB)!
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 are 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 clearly 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()
function that returns an object that has implemented IEnumerator
. Then IEnumerator
demands you implement several methods, the two key ones are the MoveNext()
function and the Current
property.
So imagine you use a For Each
loop on a object that has implemented IEnumerable
, such as this:
For Each element as Double in EnumerableMatrix
Console.WriteLine(element.ToString())
Next
What's actually happening is that the GetEnumerator()
function 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()
function and the Current
property of IEnumerator
. In fact, the code above is shorthand for this.
Dim ObjectToEnumerate As IEnumerator = EnumerableMatrix.GetEnumerator()
While ObjectToEnumerate.MoveNext() = True
Console.WriteLine(ObjectToEnumerate.Current.ToString())
End While
You could do this manually in order to gain more control over the enumeration than provided by For Each
. In the Points of Interest section, we have a quick look at the IL code and we can see directly that For Each
is turned into MoveNext()
and Current
as shown above.
To Understand Enumeration, We Must First Count to Ten
Implements IEnumerable
So, assuming we understand what a For Each
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 Function GetEnumerator() As IEnumerator(Of Double) _
Implements IEnumerable(Of Double).GetEnumerator
Return New MyNewEnumerator()
End Function
Public Function GetEnumerator1() As IEnumerator Implements Collections.IEnumerable.GetEnumerator
Return Me.GetEnumerator()
End Function
It really is that straightforward. IEnumerable
simply demands implementation of those two methods and you just point the less specific one at the other. (of course, we still have to code the enumerator class).
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 Sub New(matrix As Double(,))
Me._matrix = matrix
Me._matrixEnumerator = Demos.MatrixEnumerator.Horizontal
End Sub
A property to allow the choice between different enumeration methods and an enum
to represent the different possible enumerators.
Public Property Enumerator As MatrixEnumerator
Get
Return Me._matrixEnumerator
End Get
Set(value As MatrixEnumerator)
Me._matrixEnumerator = value
End Set
End Property
Public Enum MatrixEnumerator
Vertical
Horizontal
End Enum
And finally, some conditional code to create the desired IEnumerator
instance.
Public Function GetEnumerator() As IEnumerator(Of Double) _
Implements IEnumerable(Of Double).GetEnumerator
Select Case Me._matrixEnumerator
Case MatrixEnumerator.Horizontal
Return New HorizontalMatrixEnumerator(Me._matrix)
Case MatrixEnumerator.Vertical
Return New VerticalMatrixEnumerator(Me._matrix)
Case Else
Throw New InvalidOperationException
End Select
End Function
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 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
Implements IEnumerator(Of Double)
Private _matrix As Double(,)
Private _colIndex As Integer
Private _rowIndex As Integer
Private _curItem As Double
Private _lastCol As Integer
Private _lastRow As Integer
Public Sub New(matrix As Double(,))
Me._matrix = matrix
Me._colIndex = -1
Me._rowIndex = 0
Me._curItem = Nothing
Me._lastCol = matrix.GetUpperBound(1)
Me._lastRow = matrix.GetUpperBound(0)
End Sub
- 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 ReadOnly Property Current As Double Implements IEnumerator(Of Double).Current
Get
If IsNothing(Me._curItem) Then
Throw New InvalidOperationException()
End If
Return Me._curItem
End Get
End Property
As noted, it's just a wrapper for the _curItem
field. It throws an exception if it's Nothing
- it is initialised as such in the constructor - this ensures that we throw an exception if the user of the object (the For Each
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 Function MoveNext() As Boolean Implements IEnumerator.MoveNext
If Me._colIndex = Me._lastCol And Me._rowIndex = Me._lastRow Then
Return False
End If
If Me._colIndex = Me._lastCol Then
Me._colIndex = 0
Me._rowIndex += 1
Else
Me._colIndex += 1
End If
Me._curItem = Me._matrix(Me._rowIndex, Me._colIndex)
Return True
End Function
- 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 ... End While
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 ... End While
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 immediately 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 it does reach 2
.
Finally, the Reset()
method just puts the relevant fields back to how they were when the object was created.
Public Sub Reset() Implements IEnumerator.Reset
Me._colIndex = -1
Me._rowIndex = 0
Me._curItem = Nothing
End Sub
End Class
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 ... Then ... 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 Function MoveNext() As Boolean Implements IEnumerator.MoveNext
If Me._colIndex = Me._lastCol And Me._rowIndex = Me._lastRow Then
Return False
End If
If Me._rowIndex = Me._lastRow Then
Me._rowIndex = 0
Me._colIndex += 1
Else
Me._rowIndex += 1
End If
Me._curItem = Me._matrix(Me._rowIndex, Me._colIndex)
Return True
End Function
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 Function GetEnumerator() As IEnumerator(Of Double) _
Implements IEnumerable(Of Double).GetEnumerator
Select Case Me._matrixEnumerator
Case Demos.MatrixEnumerator.Horizontal
Return Me.HorizontalEnumerator()
Case Demos.MatrixEnumerator.Vertical
Return Me.VerticalEnumerator()
Case Else
Throw New InvalidOperationException
End Select
End Function
Essentially identical, but rather than Return New HorizontalMatrixEnumerator(Me._matrix)
, we see Return Me.HorizontalEnumerator()
, which is a method reference not an object. Then, we just need to implement those two new methods.
Private Iterator Function VerticalEnumerator() As IEnumerator(Of Double)
If IsNothing(Me._matrix) Then
Throw New InvalidOperationException()
End If
For Col As Integer = 0 To Me._matrix.GetUpperBound(1)
For Row As Integer = 0 To Me._matrix.GetUpperBound(0)
Yield Me._matrix(Row, Col)
Next
Next
End Function
Private Iterator Function HorizontalEnumerator() As IEnumerable(Of Double)
If IsNothing(Me._matrix) Then
Throw New InvalidOperationException()
End If
For Row As Integer = 0 To Me._matrix.GetUpperBound(0)
For Col As Integer = 0 To Me._matrix.GetUpperBound(1)
Yield Me._matrix(Row, Col)
Next
Next
End Function
It took me a while to figure out exactly how to declare those two methods but eventually you work out that they need to be Private Iterator Function
's with a return type of IEnumerator(Of Double)
. 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 For Each
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 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 program, the programme running the For Each
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 reaches 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 DataGridView
s as (column, row).
For Each = 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 ... End While
loop. This happens to be the code for the For Each
performed on the object which uses an IEnumerator
rather than Yield
, although it's very similar indeed for the Yield
version.
IL_0169: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<float64> _
MadBadger.Demos.ByEnumerator::GetEnumerator()
IL_016e: stloc.s VB$t_ref$L1
IL_0170: br.s IL_019b
IL_0172: ldloc.s VB$t_ref$L1
IL_0174: callvirt instance !0 class _
[mscorlib]System.Collections.Generic.IEnumerator`1<float64>::get_Current()
IL_0179: stloc.s Val
IL_017b: ldloc.s output
IL_017d: ldloc.s Val
IL_017f: ldc.i4.2
IL_0180: call float64 [mscorlib]System.Math::Round(float64, int32)
IL_0185: stloc.s VB$t_double$S0
IL_0187: ldloca.s VB$t_double$S0
IL_0189: call instance string [mscorlib]System.Double::ToString()
IL_018e: ldstr " ¦ "
IL_0193: call string [mscorlib]System.String::Concat(string, string, string)
IL_0198: stloc.s output
IL_019a: nop
IL_019b: ldloc.s VB$t_ref$L1
IL_019d: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
IL_01a2: stloc.s VB$CG$t_bool$S0
IL_01a4: ldloc.s VB$CG$t_bool$S0
IL_01a6: brtrue.s IL_0172
IL_01a8: nop
IL_01a9: leave.s IL_01c3
}
finally
{
IL_01ab: ldloc.s VB$t_ref$L1
IL_01ad: ldnull
IL_01ae: ceq
IL_01b0: ldc.i4.0
IL_01b1: ceq
IL_01b3: stloc.s VB$CG$t_bool$S0
IL_01b5: ldloc.s VB$CG$t_bool$S0
IL_01b7: brfalse.s IL_01c1
IL_01b9: ldloc.s VB$t_ref$L1
IL_01bb: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_01c0: nop
IL_01c1: nop
IL_01c2: endfinally
}
You can clearly see that at IL_0170
, it jumps (unconditionally) to the short section that includes the call to MoveNext()
, starting at IL_019b
. At the end of that section, it branches on true
(IL_01a6
) to the slightly longer section above (assuming MoveNext()
returned True
), starting at IL_0172
which then calls Current
. Completely consistent with a While True) ... End While
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_01a6
) 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 example 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
instance class [mscorlib]System.Collections.Generic.IEnumerable`1<float64>
HorizontalEnumerator () cil managed
{
.custom instance void
[mscorlib]System.Runtime.CompilerServices.IteratorStateMachineAttribute::.ctor
(class [mscorlib]System.Type) = (
01 00 3e 4d 61 64 42 61 64 67 65 72 2e 44 65 6d
6f 73 2e 42 79 59 69 65 6c 64 2b 56 42 24 53 74
61 74 65 4d 61 63 68 69 6e 65 5f 31 5f 48 6f 72
69 7a 6f 6e 74 61 6c 45 6e 75 6d 65 72 61 74 6f
72 00 00
)
.custom instance void [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = (
01 00 00 00
)
.maxstack 2
.locals init (
[0] class MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator $sm,
[1] class [mscorlib]System.Collections.Generic.IEnumerable`1<float64> HorizontalEnumerator
)
IL_0000: newobj instance
void MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::.ctor()
IL_0005: stloc.0
IL_0006: ldloc.0
IL_0007: ldarg.0
IL_0008: stfld class MadBadger.Demos.ByYield
MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$VB$Me
IL_000d: ldloc.0
IL_000e: ldc.i4.s -2
IL_0010: stfld int32 MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$State
IL_0015: ldloc.0
IL_0016: stloc.1
IL_0017: br.s IL_0019
IL_0019: ldloc.1
IL_001a: ret
}
I don't get all of that, but I do see a reference to ByYield/VB$StateMachine_1_HorizontalEnumerator::$VB$Me
. We can see that in the image just above - it's the fourth item in the ByYield
description, just under DerivedTypes
. Here's what's contained in that class member.
There's are old friends, MoveNext()
, Reset()
and the Current
property; those were not written in the ByYield
class, the compiler has created them. Clearly, 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 assembly hidebysig strict virtual
instance bool MoveNext () cil managed
{
.custom instance void _
[mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
01 00 00 00
)
.override method instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
.maxstack 5
.locals init (
[0] bool $VB$ResumableLocal_Col$1,
[1] bool VB$doFinallyBodies,
[2] class [mscorlib]System.Exception $ex,
[3] bool VB$CG$t_bool$S0,
[4] int32 VB$CG$t_i4$S0
)
IL_0000: ldc.i4.1
IL_0001: stloc.1
IL_0002: nop
.try
{
IL_0003: ldarg.0
IL_0004: ldfld int32 MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$State
IL_0009: switch (IL_00bc, IL_0021)
IL_0016: ldarg.0
IL_0017: ldfld bool
MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$Disposing
IL_001c: stloc.3
IL_001d: ldloc.3
IL_001e: brfalse.s IL_0029
IL_0020: nop
IL_0021: nop
IL_0022: ldc.i4.0
IL_0023: stloc.0
IL_0024: leave IL_0143
IL_0029: nop
IL_002a: nop
IL_002b: ldarg.0
IL_002c: ldfld class
MadBadger.Demos.ByYield
MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$VB$Me
IL_0031: ldfld float64[0..., 0...] MadBadger.Demos.ByYield::_matrix
IL_0036: call bool
[Microsoft.VisualBasic]Microsoft.VisualBasic.Information::IsNothing(object)
IL_003b: stloc.3
IL_003c: ldloc.3
IL_003d: brfalse.s IL_0045
IL_003f: newobj instance void [mscorlib]System.InvalidOperationException::.ctor()
IL_0044: throw
IL_0045: nop
IL_0046: ldarg.0
IL_0047: ldc.i4.0
IL_0048: ldarg.0
IL_0049: ldarg.0
IL_004a: ldfld class
MadBadger.Demos.ByYield MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$VB$Me
IL_004f: ldfld float64[0..., 0...] MadBadger.Demos.ByYield::_matrix
IL_0054: ldc.i4.0
IL_0055: callvirt instance int32
[mscorlib]System.Array::GetUpperBound(int32)
IL_005a: stfld int32
MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$VB$ResumableLocal_VB$t_i4$L0$1
IL_005f: stfld int32
MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$VB$ResumableLocal_Row$1
IL_0064: br IL_0103
IL_0069: ldarg.0
IL_006a: ldc.i4.0
IL_006b: ldarg.0
IL_006c: ldarg.0
IL_006d: ldfld class MadBadger.Demos.ByYield
MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$VB$Me
IL_0072: ldfld float64[0..., 0...] MadBadger.Demos.ByYield::_matrix
IL_0077: ldc.i4.1
IL_0078: callvirt instance int32 [mscorlib]System.Array::GetUpperBound(int32)
IL_007d: stfld int32
MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$VB$ResumableLocal_VB$t_i4$L1$1
IL_0082: stfld int32
MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$VB$ResumableLocal_Col$1
IL_0087: br.s IL_00e2
IL_0089: ldarg.0
IL_008a: ldarg.0
IL_008b: ldfld class
MadBadger.Demos.ByYield MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$VB$Me
IL_0090: ldfld float64[0..., 0...] MadBadger.Demos.ByYield::_matrix
IL_0095: ldarg.0
IL_0096: ldfld int32 _
MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$VB$ResumableLocal_Row$1
IL_009b: ldarg.0
IL_009c: ldfld int32 _
MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$VB$ResumableLocal_Col$1
IL_00a1: callvirt instance float64 float64[0..., 0...]::Get(int32, int32)
IL_00a6: stfld float64 MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$Current
IL_00ab: ldarg.0
IL_00ac: ldc.i4.0
IL_00ad: stfld int32 MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$State
IL_00b2: ldc.i4.0
IL_00b3: stloc.1
IL_00b4: ldc.i4.1
IL_00b5: stloc.0
IL_00b6: leave IL_0143
IL_00bb: nop
IL_00bc: nop
IL_00bd: ldarg.0
IL_00be: ldfld bool MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$Disposing
IL_00c3: stloc.3
IL_00c4: ldloc.3
IL_00c5: brfalse.s IL_00cb
IL_00c7: ldc.i4.0
IL_00c8: stloc.0
IL_00c9: leave.s IL_0143
IL_00cb: nop
IL_00cc: ldarg.0
IL_00cd: ldc.i4.m1
IL_00ce: stfld int32 MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$State
IL_00d3: nop
IL_00d4: ldarg.0
IL_00d5: dup
IL_00d6: ldfld int32
MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$VB$ResumableLocal_Col$1
IL_00db: ldc.i4.1
IL_00dc: add.ovf
IL_00dd: stfld int32
MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$VB$ResumableLocal_Col$1
IL_00e2: ldarg.0
IL_00e3: ldfld int32
MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$VB$ResumableLocal_Col$1
IL_00e8: ldarg.0
IL_00e9: ldfld int32
MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$VB$ResumableLocal_VB$t_i4$L1$1
IL_00ee: stloc.s VB$CG$t_i4$S0
IL_00f0: ldloc.s VB$CG$t_i4$S0
IL_00f2: ble.s IL_0089
IL_00f4: nop
IL_00f5: ldarg.0
IL_00f6: dup
IL_00f7: ldfld int32
MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$VB$ResumableLocal_Row$1
IL_00fc: ldc.i4.1
IL_00fd: add.ovf
IL_00fe: stfld int32
MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$VB$ResumableLocal_Row$1
IL_0103: ldarg.0
IL_0104: ldfld int32
MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$VB$ResumableLocal_Row$1
IL_0109: ldarg.0
IL_010a: ldfld int32
MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$VB$ResumableLocal_VB$t_i4$L0$1
IL_010f: stloc.s VB$CG$t_i4$S0
IL_0111: ldloc.s VB$CG$t_i4$S0
IL_0113: ble IL_0069
IL_0118: nop
IL_0119: leave.s IL_0137
IL_011b: leave.s IL_0135
}
catch [mscorlib]System.Exception
{
IL_011d: dup
IL_011e: call void _
[Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.ProjectData::SetProjectError_
(class [mscorlib]System.Exception)
IL_0123: stloc.2
IL_0124: nop
IL_0125: ldarg.0
IL_0126: ldc.i4.1
IL_0127: stfld int32 MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$State
IL_012c: rethrow
IL_012e: call void
[Microsoft.VisualBasic]Microsoft.VisualBasic.CompilerServices.ProjectData::ClearProjectError()
IL_0133: leave.s IL_0135
}
IL_0135: nop
IL_0136: nop
IL_0137: nop
IL_0138: ldarg.0
IL_0139: ldc.i4.1
IL_013a: stfld int32 MadBadger.Demos.ByYield/VB$StateMachine_1_HorizontalEnumerator::$State
IL_013f: ldc.i4.0
IL_0140: stloc.0
IL_0141: br.s IL_0143
IL_0143: ldloc.0
IL_0144: ret
}
.method public final newslot strict virtual
instance bool MoveNext () cil managed
{
.override method instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
.maxstack 4
.locals init (
[0] bool MoveNext,
[1] bool VB$CG$t_bool$S0
)
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldfld int32 MadBadger.Demos.HorizontalMatrixEnumerator::_colIndex
IL_0007: ldarg.0
IL_0008: ldfld int32 MadBadger.Demos.HorizontalMatrixEnumerator::_lastCol
IL_000d: ceq
IL_000f: ldarg.0
IL_0010: ldfld int32 MadBadger.Demos.HorizontalMatrixEnumerator::_rowIndex
IL_0015: ldarg.0
IL_0016: ldfld int32 MadBadger.Demos.HorizontalMatrixEnumerator::_lastRow
IL_001b: ceq
IL_001d: and
IL_001e: stloc.1
IL_001f: ldloc.1
IL_0020: brfalse.s IL_0026
IL_0022: ldc.i4.0
IL_0023: stloc.0
IL_0024: br.s IL_0081
IL_0026: nop
IL_0027: ldarg.0
IL_0028: ldfld int32 MadBadger.Demos.HorizontalMatrixEnumerator::_colIndex
IL_002d: ldarg.0
IL_002e: ldfld int32 MadBadger.Demos.HorizontalMatrixEnumerator::_lastCol
IL_0033: ceq
IL_0035: stloc.1
IL_0036: ldloc.1
IL_0037: brfalse.s IL_0050
IL_0039: ldarg.0
IL_003a: ldc.i4.0
IL_003b: stfld int32 MadBadger.Demos.HorizontalMatrixEnumerator::_colIndex
IL_0040: ldarg.0
IL_0041: ldarg.0
IL_0042: ldfld int32 MadBadger.Demos.HorizontalMatrixEnumerator::_rowIndex
IL_0047: ldc.i4.1
IL_0048: add.ovf
IL_0049: stfld int32 MadBadger.Demos.HorizontalMatrixEnumerator::_rowIndex
IL_004e: br.s IL_005f
IL_0050: nop
IL_0051: ldarg.0
IL_0052: ldarg.0
IL_0053: ldfld int32 MadBadger.Demos.HorizontalMatrixEnumerator::_colIndex
IL_0058: ldc.i4.1
IL_0059: add.ovf
IL_005a: stfld int32 MadBadger.Demos.HorizontalMatrixEnumerator::_colIndex
IL_005f: nop
IL_0060: ldarg.0
IL_0061: ldarg.0
IL_0062: ldfld float64[0..., 0...] MadBadger.Demos.HorizontalMatrixEnumerator::_matrix
IL_0067: ldarg.0
IL_0068: ldfld int32 MadBadger.Demos.HorizontalMatrixEnumerator::_rowIndex
IL_006d: ldarg.0
IL_006e: ldfld int32 MadBadger.Demos.HorizontalMatrixEnumerator::_colIndex
IL_0073: callvirt instance float64 float64[0..., 0...]::Get(int32, int32)
IL_0078: stfld float64 MadBadger.Demos.HorizontalMatrixEnumerator::_curItem
IL_007d: ldc.i4.1
IL_007e: stloc.0
IL_007f: br.s IL_0081
IL_0081: ldloc.0
IL_0082: ret
}
I'm not qualified to comment with any professionalism, but 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, (interestingly, the almost identical C# code produced a much smaller difference between the hand written IEnumerator
and the compiler authored version). 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 them 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 1 (21 April 2013): Edited from v5 of C# version