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.
Interface |
Purpose |
---|---|
IEnumerable |
Designates a class that can be enumerated |
Designates a class that iterates over a collection; supports the For Each loop |
|
Implemented by all collections |
|
Compares two objects; used for sorting |
|
Used by collections that can be indexed |
|
For key/value-based collections |
|
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.
|
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 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.
|
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.
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.
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 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.
Top |