Section 15.1. The Collection Interfaces

   

15.1 The Collection Interfaces

Every collection has certain shared characteristics. These are captured by the collection interfaces . The .NET Framework provides standard interfaces for enumerating, comparing, and creating collections.

Chapter 13 introduced 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.

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

Interface

Purpose

IEnumerable

Designates a class that can be enumerated

IEnumerator

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

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 Interface

In 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.

Note the subtle difference in the names of these two interfaces: IEnumer able vs. IEnumer ator . 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 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.

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 mean the same thing.

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.

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 (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 15-1, followed by a detailed analysis.

Example 15-1. 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 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

Property or Method

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 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 Debugger

The 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 Each
figs/lvbn_1501.gif

Run 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( )
figs/lvbn_1502.gif

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 expanded
figs/lvbn_1503.gif

Expand 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 expanded
figs/lvbn_1504.gif

Press 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 array
figs/lvbn_1505.gif

If you continue pressing F11, you will exit the enumerator and return to the For Each loop where the string (Hello) is displayed.

   


Learning Visual Basic. NET
Learning Visual Basic .Net
ISBN: 0596003862
EAN: 2147483647
Year: 2002
Pages: 153
Authors: Jesse Liberty

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net