Programming with Enumerators

My friend John "Ballpeen" Armitage asked me about enumerators. Having experience in a procedural language like Natural provided him with the underlying idea; he just needed a little push to bridge the gap.

In Visual Basic .NET (really the .NET framework) an enumerator is a class that implements the iterator pattern (see Erich Gamma, et al. "Design Patterns: Elements of Reusable Object-Oriented Software"). In the .NET framework the iterator pattern is supported by the IEnumerator interface. However, this won't tell you what an enumerator is unless you understand the iterator pattern. Borrowing a succinct definition from Gamma's book: an iterator provides a way to access elements of an aggregate object sequentially without exposing its underlying representation. The way I explain iterators is "separating data traversal from data type". Enumerators and iterators are synonymous; it just so happens that .NET uses the term enumerator.

Classes that implement the IEnumerator interface generally represent a collection of data and the methods in the enumerator allow you to uni-directionally traverse the elements in the collection. From the perspective of the enumerator the enumerated object is read only-that is, the number of elements are read-only-but you may modify the state of the enumerated elements. Collections are generally modifiable, but when using an enumerator the aggregate, or collection of data, is treated as containing a fixed number of elements.

In this article I will explain where you will find enumerators implicitly and explicitly and demonstrate a fun example-a form generator-that implicitly uses an enumerator to generate controls on a form on the fly.

Using the For Each Statement

Visual Basic .NET supports the For Each statement. For Each allows you to enumerate over all of the elements in an aggregate type, returning an instance of a member of the aggregate rather than an index that can be used to request an element from the aggregate. (Both For Next and For Each statements are supported in Visual Basic .NET.) As you might have guessed from my verbiage in this paragraph, the For Each statement relies on the aggregate object implementing the IEnumerable interface. The Visual Studio .NET help phrases it as: "the collection must expose Array.GetEnumerator".

For example, suppose we have a strongly typed collection (see "Implementing the Strongly Typed Collection" May, 2002) of recipes. The strongly typed collection implements the IEnumerable interface. IEnumerable has one method that consumers must realize, GetEnumerator. As a result we can iterate over each of the elements in our strongly typed collection of recipes. (I named my recipes collection, CookBook.) Listing 1 demonstrates code that implicitly uses the enumerator (and listing 2 shows the MSIL for that code), and listing 3 demonstrates an explicit use of the enumerator.

Listing 1: An enumerator is implicitly employed when you use a For Each statement.

Private Sub EnumerateRecipes(ByVal Book As CookBook)
  Dim Recipe As Recipe
  For Each Recipe In Book
    Debug.WriteLine(Recipe.Instructions)
  Next
End Sub

You can quickly see that listing 1 enumerates all of the Recipe objects in the CookBook. Aside from the help telling us how this code behaves internally we can glean information about the underlying support for the For Each statement by exploring the EnumerateRecipes method with the ildasm.exe utility. Listing 2 shows the disassembled code for EnumerateRecipes.

Listing 2: The MSIL for the EnumerateRecipes method, showing the underlying explicitly use of IEnumerator.

.method private instance void 
EnumerateRecipes(class EnumeratorDemo.CookBook Book) cil managed
{
  // Code size       72 (0x48)
  .maxstack  1
  .locals init ([0] class EnumeratorDemo.Recipe Recipe,
    [1] class [mscorlib]System.Collections.IEnumerator _Vb_t_ref_0)
  IL_0000:  nop
  IL_0001:  nop
  .try
  {
    IL_0002:  ldarg.1
    IL_0003:  callvirt   instance class 
[mscorlib]System.Collections.IEnumerator _
    [mscorlib]System.Collections.CollectionBase::GetEnumerator()
    IL_0008:  stloc.1
    IL_0009:  br.s       IL_0024
    IL_000b:  ldloc.1
    IL_000c:  callvirt   instance object 
[mscorlib]System.Collections.IEnumerator::get_Current()
    IL_0011:  castclass  EnumeratorDemo.Recipe
    IL_0016:  stloc.0
    IL_0017:  ldloc.0
    IL_0018:  callvirt   instance string 
EnumeratorDemo.Recipe::get_Instructions()
    IL_001d:  call       
void [System]System.Diagnostics.Debug::WriteLine(string)
    IL_0022:  nop
    IL_0023:  nop
    IL_0024:  ldloc.1
    IL_0025:  callvirt   
instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
    IL_002a:  brtrue.s   IL_000b
    IL_002c:  leave.s    IL_0045
  }  // end .try
  finally
  {
    IL_002e:  nop
    IL_002f:  ldloc.1
    IL_0030:  isinst     [mscorlib]System.IDisposable
    IL_0035:  brfalse.s  IL_0043
    IL_0037:  ldloc.1
    IL_0038:  castclass  [mscorlib]System.IDisposable
    IL_003d:  callvirt   
instance void [mscorlib]System.IDisposable::Dispose()
    IL_0042:  nop
    IL_0043:  nop
    IL_0044:  endfinally
  }  // end handler
  IL_0045:  nop
  IL_0046:  nop
  IL_0047:  ret
} // end of method Form1::EnumerateRecipes

The bold case statements show IEnumerator being used to support the For Each statement. (Referring to the statements in bold case only.) The first statement declares a local IEnumerator variable. The second statement invokes GetEnumerator on the CookBook object. The third and fourth statements get the object referred to by the Current property and perform a type class. The fifth statement invokes the MoveNext operation. EnumerateRecipes is rewritten in listing 3, demonstrating an explicit use of the IEnumerator interface.

Listing 3: Explicitly using an enumerator.

Private Sub EnumerateRecipes(ByVal Book As CookBook)
  Dim Enumerator As IEnumerator = Book.GetEnumerator()
  While (Enumerator.MoveNext)
    Debug.WriteLine(CType(Enumerator.Current, Recipe).Instructions)
  End While
End Sub

The explicit enumerator version is slightly more complex but produces the same result as the code found in listing 1.

Requesting an IEnumerator

If a class implements the IEnumerable interface then that class will have a method GetEnumerator that returns an instance of an object that implements IEnumerator. The class may have a property or inherit from a class that implements IEnumerable such as any System.Array class does.

When you declare an array of a specific type you are actually creating an instance of an array. For example, an array of integers is actually an instance of the System.Array type. The following code demonstrates this fact.

Dim I(5) As Integer
Dim IsArray As Boolean = TypeOf I Is System.Array

IsArray will always yield True in array declarations in Visual Basic .NET. And, because System.Array implements IEnumerable you will always be able to iterate arrays using the For Each statement or a literal enumerator.

Using an IEnumerator

IEnumerable requires that you implement a method GetEnumerator that returns an object that implements IEnumerator. IEnumerator defines three members: Reset, MoveNext, and Current. Reset moves the internal index to a position before the first element. MoveNext moves the internal pointer and returns a Boolean indicating if you have iterated past the last element, and Current returns an object representing the object at the current index position.

Caution: IEnumerator can throw an InvalidOperationException if the underlying collection of objects changes, for example, if an element is deleted from the array while you are enumerating.

The general usage of an IEnumerator is to invoke MoveNext and interact with the Current property in a loop. (See listing 3 for an example.)

Example Program

For fun I created a sample class that uses the For Each statement. The sample class is named Generator. If you construct an instance of Generator with a parent control, the type of an object, and an object that implements IEnumerable, such as an array, then Generator will add dynamically created Windows controls to the parent control object. The code is provided (in listing 4) for you to experiment with without elaboration. I hope you enjoy it.

Listing 4: Generator uses an enumerator implicitly to discover the properties of a type, generating a dynamic Windows Forms interface.

Imports System.Windows.Forms
Imports System.Reflection
Imports System.Drawing

Public Class Generator
  Private FParent As Control
  Private FType As Type
  Private FData As Object

  Public Sub New(ByVal Parent As Control, _
   ByVal AType As Type, ByVal Data As Object)
    FParent = Parent
    FType = AType
    FData = Data
  End Sub

  Public Sub AddControls()

    Dim [Property] As PropertyInfo
    For Each [Property] In FType.GetProperties()
      AddControl([Property], FData)
    Next

    ApplyLayout()
  End Sub

  Public Sub ApplyLayout(Optional ByVal Pad As Integer = 4)
    Dim WidestLabel As Control = GetWidestLabel()
    Dim Target As Control

    For Each Target In FParent.Controls
      If (TypeOf Target Is Label = False) Then
        Target.Left = WidestLabel.Left + _
          WidestLabel.Width + Pad
      End If
    Next
  End Sub

  Public Function GetMaxWidth() As Integer
    Return GetWidestLabel.Width
  End Function

  Public Function GetWidestLabel() As Control
    Dim AControl As Control = Nothing

    For Each AControl In FParent.Controls
      If (TypeOf AControl Is Label) Then

        If (GetWidestLabel Is Nothing) Then
          GetWidestLabel = AControl
        Else
          If (AControl.Width > GetWidestLabel.Width) Then
            GetWidestLabel = AControl
          End If
        End If

      End If
    Next

  End Function

  Public Sub AddControl(ByVal Prop As PropertyInfo, _
    ByVal Data As Object)
    AddControl(FParent, Prop, Data)
  End Sub

  Public Shared Sub AddControl(ByVal Parent As Control, _
    ByVal Prop As PropertyInfo, ByVal Data As Object)

    Dim ALabel As Label = New Label()
    ALabel.AutoSize = True
    ALabel.Text = Prop.Name
    ALabel.Location = New Point(10, Parent.Controls.Count * 15 + 5)

    Parent.Controls.Add(ALabel)

    Dim ATextBox As TextBox = New TextBox()
    ATextBox.DataBindings.Add("Text", Data, Prop.Name)
    ATextBox.AutoSize = True
    ATextBox.Width = 200
    ATextBox.Location = New Point(150, ALabel.Top)
    Parent.Controls.Add(ATextBox)

  End Sub


  Public Sub ClearParent()
    ClearParent(FParent)
  End Sub

  Public Shared Sub ClearParent(ByVal Parent As Control)
    Parent.Controls.Clear()
  End Sub

  Public Sub HandlePrevious(ByVal Sender As Object, _
    ByVal e As System.EventArgs)

    Try
      CType(FParent, _
        Control).BindingContext(FData).Position -= 1
    Catch
    End Try

  End Sub

  Public Sub HandleNext(ByVal Sender As Object, _
    ByVal e As System.EventArgs)
    Try
      CType(FParent, _
        Control).BindingContext(FData).Position += 1
    Catch
    End Try
  End Sub

End Class

Summary

Loop statements are one of the fundamental building blocks of many programming languages. Visual Basic .NET is completely comprised of classes. It would be frustrating if Visual Basic .NET did not support simple looping constructs. Of course Visual Basic .NET does; it is simply a matter of Visual Basic .NET supporting loop iteration in an object-oriented way.

Use the For Each statement because it is simpler but understand that underneath the very simple loop lies the Iterator pattern. Implemented as the IEnumerator interface, the iterator pattern allows you to separate iterating data from the type of the data. This is a simple, yet powerful concept.

About the Author

Paul Kimmel is a freelance writer for Developer.com and CodeGuru.com. Look for his recent book, Visual Basic .Net Unleashed, at a bookstore near you. Paul Kimmel is available to help design and build your .NET solutions and can be contacted at pkimmel@softconcepts.com.



Comments

  • http://www.oakleysunglassesoutc.com/ gdcfhx

    Posted by http://www.oakleysunglassesoutc.com/ Mandydpi on 03/28/2013 07:19pm

    ghd straightener,ghd australia,Given town two ship in the year total cost of 3.402 million, should also not to the British shipyard Inquiry, but estimates down will not be less than 4,002,000 ... group security is to a large ship purchase , small ship self-made? Li Hongzhang asked. Tan Yankai nodded and said: The old material relative to the country is not bad, the Younger exactly think ground. If all goes well, buy a British main warships self-made three cruisers, plus Beiyang its six main warships, while the older relative to the country in times of war to buy Anglo-German torpedo boat few of the day should arrive in Tianjin. So you figure the strength of the fleet has been far more than the previous Northern navy. Li Hongzhang and metalloproteinase phase, as the smile: If then the court be able to continue to provide funding to the Northern navy. Old relative to the country, it has passed, ghd hair straightener,hairstraighteneraul.ghd,com/" title="cheap ghd"cheap ghd In addition to the lessons learned, now remaining only to work hard to catch up.

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: April 22, 2014 @ 1:00 p.m. ET / 10:00 a.m. PT Database professionals — whether developers or DBAs — can often save valuable time by learning to get the most from their new or existing productivity tools. Whether you're responsible for managing database projects, performing database health checks and reporting, analyzing code, or measuring software engineering metrics, it's likely you're not taking advantage of some of the lesser-known features of Toad from Dell. Attend this live …

  • On-demand Event Event Date: March 27, 2014 Teams need to deliver quality software faster and need integrated agile planning, task tracking, source control, auto deploy with continuous builds and a configurable process to adapt to the way you work. Rational Team Concert and DevOps Services (JazzHub) have everything you need to build great software, integrated seamlessly together right out of the box or available immediately in the cloud. And with the Rational Team Concert Client, you can connect your …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds