DekGenius.com
Team LiB   Previous Section   Next Section

16.1 The Collection Interfaces

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

Chapter 14 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 provides the same semantics as the collection classes available through the .NET Framework. Table 16-1 lists the key collection interfaces and their uses.

Table 16-1. The collection interfaces

Interface

Purpose

IEnumerable

Enumerates through a collection using a foreach statement

IEnumerator

Iterates over a collection and supports the foreach 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 such as Hashtable and SortedList

IDictionaryEnumerator

Allows enumeration with foreach of a collection that supports IDictionary

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 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";
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.

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.

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.

Note the subtle difference in the names of these two interfaces. IEnumerable vs. IEnumerator. The former designates a class that can be enumerated, the latter designates a class that does the actual enumeraion.

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.

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), 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. Enumeration
using 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.

Table 16-2. IEnumerator

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 ListBoxEnumerator(ListBoxTest currentListBox)
{
    this.currentListBox = currentListBox;
    index = -1;
}

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

The 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 foreach
figs/lcs_1601.gif

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

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

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

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

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

    Team LiB   Previous Section   Next Section