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.