Previous section   Next section

9.5 The Collection Interfaces: IEnumerable

While the Array is the simplest type of collection, there are times when you need additional functionality. The .NET Framework provides a number of already built and tested collection classes, including the ArrayList, Collection, Queue, and Stack. These standard classes are covered later in this chapter.

Chapter 8 introduced the concept of interfaces, which create a contract that a class can fulfill. Implementing an interface allows clients of the class to know exactly what to expect from the class. The .NET Framework provides a number of standard interfaces for enumerating, comparing, and creating collections.

These collection interfaces make it possible for you to write your own custom collection classes. By implementing the collection interfaces, your custom classes can provide the same semantics as the collection classes available through the .NET Framework. Table 9-2 lists the key collection interfaces and their uses.

Table 9-2. The collection interfaces

Interface

Purpose

IEnumerable

Designates a class that can be enumerated

IEnumerator

Designates a class that iterates over a collection; supports the For Each loop

ICollection

Implemented by all collections

IComparer

Compares two objects; used for sorting

IList

Used by collections that can be indexed

IDictionary

For key/value-based collections

IDictionaryEnumerator

Allows enumeration with For Each of a collection that supports IDictionary

This section will focus on the IEnumerable interface, using it to demonstrate how you can implement the collection interfaces in your own classes (allowing clients to treat your custom classes as if they were collections).

In Example 9-10, you developed a simple ListBoxTest class that provided an indexer for array-like semantics. That is, your ListBoxTest implemented its own indexer, so that you could treat the ListBoxTest object like it was an array:

myListBoxTest(5) = "Hello world"
Dim theText As String = myListBoxTest(1)

Of course, ListBoxTest is not an array; it is just a custom class that can be treated like an array because you gave it this indexer. You can make your ListBoxTest class even more like a real array by providing support for iterating over the contents of the array using the For Each statement. To provide support for the For Each statement, you'll need to implement the IEnumerable interface.

When you iterate over an array you visit each member in turn. Programmers talk about iterating over an array, iterating the array, iterating through the array, and enumerating the array. All of these terms are interchangeable.

The For Each statement will work with any class that implements the IEnumerable interface. Classes that implement the IEnumerable interface have a single method, GetEnumerator( ), that returns an object that implements a second interface, IEnumerator.

Note the subtle difference in the names of these two interfaces: I Enumerableversus IEnumerator. The former designates a class that can be enumerated, the latter designates a class that does the actual enumeration.

The entire job of the IEnumerable interface is to define the GetEnumerator( ) method. The job of the GetEnumerator( ) method is to generate a specialized enumerator�that is, an instance of a class that implements a second interface, the IEnumerator interface. A class that implements IEnumerable can be enumerated. A class that implements IEnumerator knows how to enumerate an enumerable class (i.e., one that implements IEnumerable).

By implementing the IEnumerable interface, your ListBoxTest class is saying "you can enumerate my members, just ask me for my enumerator." The client asks the ListBoxTest for its enumerator by calling the GetEnumerator( ) method. What it gets back is an instance of a class that knows how to iterate over a listbox. That class, ListBoxEnumerator, will implement the IEnumerator interface.

This gets a bit confusing, so let's use an example. When you implement the IEnumerable interface for ListBoxTest, you are promising potential clients that ListBoxTest will support enumeration. That allows clients of your ListBoxTest class to write code like this:

Dim s As String
For Each s In ListBoxText
   '...
Next

You implement IEnumerable by providing the GetEnumerator( ) method, which returns an implementation of the IEnumerator interface. In this case, you'll return an instance of the ListBoxEnumerator class, and ListBoxEnumerator will implement the IEnumerator interface:

Public Function GetEnumerator( ) As IEnumerator _
        Implements IEnumerable.GetEnumerator
            Return New ListBoxEnumerator(Me)
End Function

The ListBoxEnumerator is a specialized instance of IEnumerator that knows how to enumerate the contents of your ListBoxTest class. Notice two things about this implementation. First, the constructor for ListBoxEnumerator takes a single argument, and you pass in the Me keyword. Doing so passes in a reference to the current ListBoxTest object, which is the object that will be enumerated. Second, notice that the ListBoxEnumerator is returned as an instance of IEnumerator. This implicit cast is safe because the ListBoxEnumerator class implements the IEnumerator interface.

An alternative to creating a specialized class to implement IEnumerator is to have the enumerable class (ListBoxTest) implement IEnumerator itself. In that case, the IEnumerator returned by GetEnumerator( ) would be the ListBoxTest object, cast to IEnumerator.

Putting the enumeration responsibility into a dedicated class that implements IEnumerator (e.g., ListBoxEnumerator) is generally preferred to the alternative of letting the collection class (ListBoxTest) know how to enumerate itself. The specialized enumeration class encapsulates the responsibility of enumeration and the collection class (ListBoxTest) is not cluttered with a lot of enumeration code.

Because ListBoxEnumerator is specialized to know only how to enumerate ListBoxTest objects (and not any other enumerable objects), you will make ListBoxEnumerator a private class, contained within the definition of ListBoxTest. (The collection class is often referred to as the container class because it contains the members of the collection.) The complete listing is shown in Example 9-12, followed by a detailed analysis.

Example 9-12. Enumeration
Option Strict On

Imports System
Imports System.Collections

Namespace Enumeration

    Public Class ListBoxTest : Implements IEnumerable
        Private strings( ) As String
        Private ctr As Integer = 0

        'private nested implementation of ListBoxEnumerator
        Private Class ListBoxEnumerator
            Implements IEnumerator
            'member fields of the nested ListBoxEnumerator class
            Private currentListBox As ListBoxTest
            Private index As Integer

            'public within the private implementation
            'thus, private within ListBoxTest
            Public Sub New(ByVal currentListBox As ListBoxTest)
                'a particular ListBoxTest instance is
                'passed in, hold a reference to it
                'in the member varaible currentListBox. 
                Me.currentListBox = currentListBox
                index = -1
            End Sub

            'increment the index and make sure the
            'value is valid
            Public Function MoveNext( ) As Boolean _
              Implements IEnumerator.MoveNext
                index += 1
                If index >= currentListBox.strings.Length Then
                    Return False
                Else
                    Return True
                End If
            End Function

            Public Sub Reset( ) _
              Implements IEnumerator.Reset
                index = -1
            End Sub

            'current property defined as the
            'last string added to the listbox
            Public ReadOnly Property Current( ) As Object _
            Implements IEnumerator.Current
                Get
                    Return currentListBox(index)
                End Get
            End Property
        End Class  ' end nested class

        'enumerable classes can return an enumerator
        Public Function GetEnumerator( ) As IEnumerator _
        Implements IEnumerable.GetEnumerator
            Return New ListBoxEnumerator(Me)
        End Function

        'initialize the listbox with strings
        Public Sub New( _
          ByVal ParamArray initialStrings( ) As String)
            'allocate space for the strings
            ReDim strings(7)

            'copy the strings passed in to the constructor
            Dim s As String
            For Each s In initialStrings
                strings(ctr) = s
                ctr += 1
            Next
        End Sub

        'add a single string to the end of the listbox
        Public Sub Add(ByVal theString As String)
            strings(ctr) = theString
            ctr += 1
        End Sub

        'allow array-like access
        Default Public Property Item( _
          ByVal index As Integer) As String
            Get
                If index < 0 Or index >= strings.Length Then
                    ' handle bad index
                    Exit Property
                End If
                Return strings(index)
            End Get
            Set(ByVal Value As String)
                strings(index) = Value
            End Set
        End Property

        'publish how many strings you hold
        Public Function GetNumEntries( ) As Integer
            Return ctr
        End Function

    End Class

    Public Class Tester
        Public Sub Run( )
            'create a new listbox and initialize
            Dim currentListBox As New _
                ListBoxTest("Hello", "World")

            'add a few strings
            currentListBox.Add("Who")
            currentListBox.Add("Is")
            currentListBox.Add("John")
            currentListBox.Add("Galt")

            'test the access
            Dim subst As String = "Universe"
            currentListBox(1) = subst

            'access all the strings
            Dim s As String
            For Each s In currentListBox
                Console.WriteLine("Value: {0}", s)
            Next
        End Sub

        Shared Sub Main( )
            Dim t As New Tester( )
            t.Run( )
        End Sub
    End Class

End Namespace

Output:
Value: Hello
Value: Universe
Value: Who
Value: Is
Value: John
Value: Galt
Value:
Value:

The GetEnumerator( ) method of ListBoxTest passes a reference to the current object ListBoxEnumerator( ) to the enumerator, using the Me keyword:

Return New ListBoxEnumerator(Me)

The enumerator will enumerate the members of the ListBoxTest object passed in as a parameter.

The class to implement the Enumerator is implemented as ListBoxEnumerator. The most interesting aspect of this code is the definition of the ListBoxEnumerator class. Notice that this class is defined within the definition of ListBoxTest. It is a nested class. It is also marked private; the only method that will ever instantiate a ListBoxEnumerator object is the GetEnumerator( ) method of ListBoxTest:

'private nested implementation of ListBoxEnumerator
Private Class ListBoxEnumerator
     Implements IEnumerator

ListBoxEnumerator is defined to implement the IEnumerator interface, which defines one property and two methods, as shown in Table 9-3.

Table 9-3. IEnumerator

Method or property

Description

Current

Property that returns the current element

MoveNext( )

Method that advances the enumerator to the next element

Reset( )

Method that sets the enumerator to its initial position, before the first element

The ListBoxTest object to be enumerated is passed in as an argument to the ListBoxEnumerator constructor, where it is assigned to the member variable currentListBox. The constructor also sets the member variable index to -1, indicating that you have not yet begun to enumerate the object:

Public Sub New(ByVal currentListBox As ListBoxTest)
    Me.currentListBox = currentListBox
    index = -1
End Sub

The number -1 is used as a signal to indicate that the enumerator is not yet pointing to any of the elements in the ListBoxTest object. You can't use the value 0, because 0 is a valid offset into the collection.

The MoveNext( ) method increments the index and then checks the length property of the strings array to ensure that you've not run past the end of the strings array. If you have run past the end, you return false; otherwise, you return true:

Public Function MoveNext( ) As Boolean _
  Implements IEnumerator.MoveNext
    index += 1
    If index >= currentListBox.strings.Length Then
        Return False
    Else
        Return True
    End If
End Function

The IEnumerator method Reset( ) does nothing but reset the index to -1. You can call Reset( ) any time you want to start over iterating the ListBoxTest object.

The Current property is implemented to return the current string. This is an arbitrary decision; in other classes, Current will have whatever meaning the designer decides is appropriate. However defined, every enumerator must be able to return the current member, as accessing the current member is what enumerators are for. The interface defines the Current property to return an object. Since strings are derived from Object, there is an implicit cast of the string to the more general Object type:

Public ReadOnly Property Current( ) As Object _
Implements IEnumerator.Current
    Get
        Return currentListBox(index)
    End Get
End Property

The call to For Each fetches the enumerator and uses it to enumerate over the array. Because For Each will display every string, whether or not you've added a meaningful value, in this example the strings array is initialized to hold only eight strings.

Now that you've seen how ListBoxTest implements IEnumerable, let's examine how the ListBoxTest object is used. The program begins by creating a new ListBoxTest object and passing two strings to the constructor:

Public Class Tester
    Public Sub Run( )
        Dim currentListBox As New _
            ListBoxTest("Hello", "World")

When the ListBoxTest object (currentListBox) is created, an array of String objects is created with room for eight strings. The initial two strings passed in to the constructor are added to the array:

Public Sub New( _
  ByVal ParamArray initialStrings( ) As String)

    ReDim strings(7)

    Dim s As String
    For Each s In initialStrings
        strings(ctr) = s
        ctr += 1
    Next
End Sub

Back in Run( ), four more strings are added using the Add( ) method, and the second string is updated with the word "Universe":

currentListBox.Add("Who")
currentListBox.Add("Is")
currentListBox.Add("John")
currentListBox.Add("Galt")

Dim subst As String = "Universe"
currentListBox(1) = subst

You iterate over the strings in currentListBox with a For Each loop, displaying each string in turn:

Dim s As String
For Each s In currentListBox
    Console.WriteLine("Value: {0}", s)
Next

The For Each loop checks that your class implements IEnumerable (and throws an exception if it does not) and invokes GetEnumerator( ):

Public Function GetEnumerator( ) As IEnumerator _
Implements IEnumerable.GetEnumerator
    Return New ListBoxEnumerator(Me)
End Function

GetEnumerator calls the ListBoxEnumerator constructor, thus initializing the index to -1.

Public Sub New(ByVal currentListBox As ListBoxTest
    Me.currentListBox = currentListBox
    index = -1
End Sub

The first time through the loop, For Each automatically invokes MoveNext( ), which immediately increments the index to 0 and returns true:

Public Function MoveNext( ) As Boolean _
  Implements IEnumerator.MoveNext
    index += 1
    If index >= currentListBox.strings.Length Then
        Return False
    Else
        Return True
    End If
End Function

The For Each loop then uses the Current property to get back the current string:

Public ReadOnly Property Current( ) As Object _
Implements IEnumerator.Current
    Get
        Return currentListBox(index)
    End Get
End Property

The Current property invokes the ListBoxTest's indexer, getting back the string stored at index 0. This string is assigned to the variable s defined in the For Each loop, and that string is displayed on the console. The For Each loop repeats these steps (call MoveNext( ), access the Current property, display the string) until all the strings in the ListBoxTest object have been displayed.


  Previous section   Next section
Top