Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Pick Your Enumerator & Me.Understand Yield and IEnumerable (VB)

4.98/5 (13 votes)
2 Nov 2023CPOL17 min read 32.1K   408  
This is an alternative for Pick Your Enumerator & Me.Understand Yield and IEnumerable (C#)

Screenshot showing an enumerated DataGridView

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?

  1. Beginners
  2. Anyone who hasn't implemented IEnumerable, (via an IEnumerator or Yield) before and wants an example
  3. Anyone who wants to implement more than one enumerator for an IEnumerable class
  4. 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

  1. We can use Yield as an alternative to creating an IEnumerator class whilst implementing IEnumerable (and specifically the GetEnumerator() method)
  2. 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
  3. A class that is IEnumerable can have more than one enumerator, user selectable at run time
  4. You can make a class enumerable without declaring it IEnumerable but by implementing a method with a type of IEnumerable
    1. Implementing more than one method that has an IEnumerable return type has the same effect as #3
  5. 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:

VB.NET
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.

VB.NET
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.

VB.NET
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.

VB.NET
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.

VB.NET
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.

VB.NET
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.

VB.NET
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.

VB.NET
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().

VB.NET
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.

Traversing the array horizontally

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.

VB.NET
    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).

VB.NET
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.

VB.NET
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.

VB.NET
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:

  1. Recognising that the code looks really very similar to the MoveNext() methods (in essence), only it's simpler
  2. 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 DataGridViews 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.

MSIL
    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
    // loop start (head: 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
    // end loop

    IL_01a8: nop
    IL_01a9: leave.s IL_01c3
} // end .try
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
} // end handler

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.

The ByYield structure by ILSpy

There's our HorizontalEnumerator method highlighted, now what does the IL code say?

MSIL
.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
    )
    // Method begins at RVA 0x2ae4
    // Code size 27 (0x1b)
    .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
} // end of method ByYield::HorizontalEnumerator

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.

The ByYield structure by ILSpy

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.

MSIL
.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()
    // Method begins at RVA 0x5830
    // Code size 325 (0x145)
    .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
    } // end .try
    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
    } // end handler

    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
} // end of method VB$StateMachine_1_HorizontalEnumerator::MoveNext
MSIL
.method public final newslot strict virtual 
    instance bool MoveNext () cil managed 
{
    .override method instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
    // Method begins at RVA 0x2268
    // Code size 131 (0x83)
    .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
} // end of method HorizontalMatrixEnumerator::MoveNext

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)