16.1 The Collection InterfacesEvery collection has certain shared characteristics, which 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 provides the same semantics as the collection classes available through the .NET Framework. Table 16-1 lists the key collection interfaces and their uses.
This 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 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 allows 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. 16.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"; string theText = 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 foreach statement. To provide support for the foreach statement, you'll need to implement the IEnumerable interface.
The foreach 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 list box. 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: foreach (string s in ListBoxTest) { //... } You implement IEnumerable by providing the GetEnumerator() method, which returns an implementation of IEnumerator. In this case, you'll return an instance of ListBoxEnumerator, and ListBoxEnumerator will implement the IEnumerator interface: public IEnumerator GetEnumerator() { return (IEnumerator) new ListBoxEnumerator(this); } 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 this reference. 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 cast to an IEnumerator before it is returned. This 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), make ListBoxEnumerator a private class, contained within the definition of ListBoxTest. The complete listing is shown in Example 16-1, followed by a detailed analysis. Example 16-1. Enumerationusing System; namespace Enumeration { using System; using System.Collections; // implements IEnumerable public class ListBoxTest : IEnumerable { private string[] strings; private int ctr = 0; // private nested implementation of ListBoxEnumerator private class ListBoxEnumerator : IEnumerator { // member fields of the nested ListBoxEnumerator class private ListBoxTest currentListBox; private int index; // public within the private implementation // thus, private within ListBoxTest public ListBoxEnumerator(ListBoxTest currentListBox) { // a particular ListBoxTest instance is // passed in, hold a reference to it // in the member variable currentListBox. this.currentListBox = currentListBox; index = -1; } // Increment the index and make sure the // value is valid public bool MoveNext() { index++; if (index >= currentListBox.strings.Length) return false; else return true; } public void Reset() { index = -1; } // Current property defined as the // last string added to the listbox public object Current { get { return(currentListBox[index]); } } } // end nested class // Enumerable classes can return an enumerator public IEnumerator GetEnumerator() { return (IEnumerator) new ListBoxEnumerator(this); } // initialize the listbox with strings public ListBoxTest(params string[] initialStrings) { // allocate space for the strings strings = new String[8]; // copy the strings passed in to the constructor foreach (string s in initialStrings) { strings[ctr++] = s; } } // add a single string to the end of the listbox public void Add(string theString) { strings[ctr] = theString; ctr++; } // allow array-like access public string this[int index] { get { if (index < 0 || index >= strings.Length) { // handle bad index } return strings[index]; } set { strings[index] = value; } } // publish how many strings you hold public int GetNumEntries() { return ctr; } } class Tester { public void Run() { // create a new listbox and initialize ListBoxTest currentListBox = new ListBoxTest("Hello", "World"); // add a few strings currentListBox.Add("Who"); currentListBox.Add("Is"); currentListBox.Add("John"); currentListBox.Add("Galt"); // test the access string subst = "Universe"; currentListBox[1] = subst; // access all the strings foreach (string s in currentListBox) { Console.WriteLine("Value: {0}", s); } } [STAThread] static void Main() { Tester t = new Tester(); t.Run(); } } } Output: Value: Hello Value: Universe Value: Who Value: Is Value: John Value: Galt Value: Value: The definition of the ListBoxEnumerator class is the most interesting aspect of this code. 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 class ListBoxEnumerator : IEnumerator { ListBoxEnumerator is defined to implement the IEnumerator interface, which defines one property and two methods, as shown in Table 16-2.
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 ListBoxEnumerator(ListBoxTest currentListBox) { this.currentListBox = currentListBox; index = -1; }
The MoveNext() method increments the index and then checks the length property of the strings array to ensure that you have not run past the end of the strings array. If you have run past the end, you return false; otherwise you return true: public bool MoveNext() { index++; if (index >= currentListBox.strings.Length) return false; else return true; } 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 has 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 object Current { get { return(currentListBox[index]); } } The call to foreach fetches the enumerator and uses it to enumerate over the array. Because foreach displays every string, whether or not you've added a meaningful value, in Example 16-1 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 void Run() { ListBoxTest currentListBox = new ListBoxTest("Hello", "World"); When the ListBoxTest object (currentListBox) is created, an array of Strings is created with room for eight strings. The initial two strings passed in to the constructor are added to the array. public ListBoxTest(params string[] initialStrings) { // allocate space for the strings strings = new String[8]; // copy the strings passed in to the constructor foreach (string s in initialStrings) { strings[ctr++] = s; } } .. 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 15-11. currentListBox.Add("Who"); currentListBox.Add("Is"); currentListBox.Add("John"); currentListBox.Add("Galt"); string subst = "Universe"; currentListBox[1] = subst; You iterate over the strings in currentListBox with a foreach loop, displaying each string in turn: foreach (string s in currentListBox) { Console.WriteLine("Value: {0}", s); } The foreach loop checks that your class implements IEnumerable (and throws an exception if it does not) and invokes GetEnumerator(): public IEnumerator GetEnumerator() { return (IEnumerator) new ListBoxEnumerator(this); } GetEnumerator() calls the ListBoxEnumerator constructor, thus initializing the index to -1. public ListBoxEnumerator(ListBoxTest currentListBox) { this.currentListBox = currentListBox; index = -1; } The first time through the loop, the foreach loop automatically invokes MoveNext(), which immediately increments the index to 0 and returns true. public bool MoveNext() { index++; if (index >= currentListBox.strings.Length) return false; else return true; } The foreach loop then uses the Current property to get back the current string. public object Current { get { return(currentListBox[index]); } } 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 foreach loop, and that string is displayed on the console. The foreach loop repeats these steps (call MoveNext(), access the Current property, display the string) until all the strings in the ListBoxTest object have been displayed. 16.1.2 Walking Through foreach in a DebuggerThe calls to MoveNext() and Current are done for you by the foreach construct; you will not see these invoked directly, though you can step into the methods in the debugger as you iterate through the foreach loop. The debugger makes the relationships among the foreach construct, the ListBoxTest class, and its enumerator explicit. To examine these relationships, put a breakpoint at the foreach loop, as shown in Figure 16-1. Figure 16-1. Setting a breakpoint on foreachRun the application to the breakpoint by pressing the F5 key. Press F11 to step into the foreach loop, and you are in the MoveNext() method of the ListBoxEnumerator. (There is no explicit call to this method, but the method is invoked by the foreach construct itself.) Notice the Autos window shows the this reference and the index (currently -1), both circled and highlighted in Figure 16-2. Figure 16-2. The Autos window in MoveNext()Now expand the this reference in the Autos window. You'll see the CurrentListBox as a property. Expand that property to see the strings as a property, as well as ctr, indicating that there are six strings so far, as shown in Figure 16-3. Figure 16-3. The Autos window with this expandedExpand the strings member variable and see the six strings, nicely tucked away in the strings array, in the order you added them. This is shown in Figure 16-4. Figure 16-4. The strings expandedPress the F11 key once. This increments the index property from -1 to zero. The index property is listed in red in the Autos window. (Each time a value changes, it is marked in red.) The MoveNext() method tests whether the index (0) is greater than the Length 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 foreach loop. Pressing F11 again moves the highlight to the string in the foreach statement, and one more press of F11 steps you into the Current property's accessor. Continue pressing F11, and 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 16-5. Figure 16-5. Indexing into the strings arrayIf you continue pressing F11, you will exit the enumerator and return to the foreach loop where the string (Hello) is displayed. |