15.1 The Collection InterfacesEvery collection has certain shared characteristics. These are captured by the collection interfaces . The .NET Framework provides standard interfaces for enumerating, comparing, and creating collections.
By implementing the collection interfaces, your custom class can provide the same semantics as the collection classes available through the .NET Framework. Table 15-1 lists the key collection interfaces and their uses. Table 15-1. The collection interfaces
The current chapter will focus on the IEnumerable interface, using it to demonstrate how you can implement the collection interfaces in your own classes to allow clients to treat your custom classes as if they were collections. For example, you might create a custom class named ListBoxTest. Your ListBoxTest class will have a set of strings to be displayed. You can implement the collection interfaces in your ListBoxTest class to allow clients to treat your ListBoxTest as if it were a collection. This will allow clients to add to the ListBoxTest using the index operator (e.g., myListBox(5) = "New String "), to sort the ListBoxTest, to enumerate the elements of the ListBoxTest, and so forth. 15.1.1 The IEnumerable InterfaceIn the previous chapter, 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. 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.
The entire job of the IEnumerable interface is to define the GetEnumerator( ) method. The job of the GetEnumerator( ) method is to generate an enumerator ”that is, an instance of a class that implements the IEnumerator interface. 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 will allow 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.
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 15-1, followed by a detailed analysis. Example 15-1. EnumerationOption 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 variable 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 list box 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 list box 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 list box 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 contained 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 15-2. Table 15-2. IEnumerator members
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 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 string at the index. 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," just as in Example 14-11. 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. 15.1.2 Walking Through the For Each Loop in a DebuggerThe calls to MoveNext( ) and Current are done for you by the For Each construct; you will not see these invoked directly, though you can step into the methods in the debugger as you iterate through the For Each loop. The debugger makes the relationships among the For Each construct, the ListBoxTest class, and its enumerator explicit. To examine these relationships, put a breakpoint at the For Each loop, as shown in Figure 15-1. Figure 15-1. Setting a breakpoint on For EachRun the application to the breakpoint by pressing the F5 key. Press F11 to step into the For Each loop, and you'll find that you are in the MoveNext( ) method of the ListBoxEnumerator. (There is no explicit call to this method, but the method is invoked by the For Each construct itself.) Notice the Locals window shows the Me reference and the index (currently -1), both circled and highlighted in Figure 15-2. Figure 15-2. The Locals window in MoveNext( )Now expand the Me reference in the Locals window. You'll see the CurrentListBox as a property. Expand that property and you'll see the strings as a property, as well as ctr, indicating that there are six strings so far, as shown in Figure 15-3. Figure 15-3. The Locals window with Me expandedExpand the strings member variable and you'll see the six strings, nicely tucked away in the strings array, in the order you added them. This is shown in Figure 15-4. Figure 15-4. The strings expandedPress the F11 key once. This increments the index property from -1 to 0. You'll see the index property listed in red in the Locals window. (Each time a value changes, it is marked in red.) The MoveNext( ) method tests whether the index (0) is greater than the Length property of the array (8). Since at this point it is not, MoveNext( ) returns true, indicating that you have not exceeded the bounds of the array but instead have moved to the next valid value in the collection. Press F11 repeatedly, until you return to the For Each loop. Pressing F11 again moves the highlight to the string in the For Each statement, and one more press of F11 steps you into the Current property's accessor. Continue pressing F11, you'll step into the indexer of the ListBoxTest class, where the current index (0) is used as an index into the internal strings array, as shown in Figure 15-5. Figure 15-5. Indexing into the strings arrayIf you continue pressing F11, you will exit the enumerator and return to the For Each loop where the string (Hello) is displayed. |