Team LiB
Previous Section Next Section

C#'s Collection Classes

Back in Chapter 6, we discussed the need for a convenient way to collect references to objects as we create them, so that we may iterate over them, retrieve a particular object on demand, and so forth. We learned that the way to do so in an OOPL is to create a special type of object called a collection, and that one of the simplest collection types in C# is the fixed-size array. When we introduced arrays in Chapter 6, we alluded to the fact that they are objects in the C# language; as it turns out, the System.Array class is the basis for all arrays. We'll revisit arrays in this section to learn about some of the more interesting features of the System.Array class.

As we discussed in Chapter 6, it's often impossible to anticipate how many of a given object type we're going to have to create as an application is running, and so using fixed-size arrays to store varying numbers of objects is often inefficient. The System.Collections namespace of the .NET FCL defines a number of alternative collection classes that can be used to store object collections. In this section, we'll discuss two of the most commonly used collection classes that we plan on using in building the SRS—the ArrayList and Hashtable classes.

Note?/td>

Note that the System.Array class isn't part of the System.Collections namespace; nonetheless, all C# collection classes—System.Array as well as the various System.Collections classes—implement the ICollection interface, which is defined in the System.Collections namespace, and hence share a common set of behaviors.

Arrays, Revisited

The System.Array class defines a variety of useful methods and properties that can be used to do such things as search, sort, modify, and determine the length of arrays.

Array Length Property

The most commonly used Array property is the Length property. It's of type int and represents the total number of elements in an array across all of its dimensions. The following snippet shows the Length property in use for a one-dimensional array:

int[] x = new int[20];

// details of array content initialization omitted ...

// Step through the array.
// Stop BEFORE i equals x.Length!!!!
for (int i = 0; i < x.Length; i++) {
  Console.WriteLine(x[i]);
}

Because arrays are zero-based, we always need to stop just one short of the length when using it as an upper bound in a for loop, as the preceding example illustrates.

Note that the length of an array doesn't reflect how many elements have been explicitly assigned values because, technically speaking, even if nothing is stored explicitly in an array, its elements will be automatically filled with zero-equivalent values suitable for the array's type, as was discussed in Chapter 6. Rather, the length of an array simply represents the total capacity of the array in terms of the total number of items that it can hold; the capacity is fixed when an array is first declared, and can't be changed thereafter.

Note?/td>

In Chapter 6 we introduced the ArrayList class that represents a collection whose size can "grow gracefully" as needed.We'll talk more about ArrayLists later in this section.

Array Methods

The Array class declares a variety of useful static and instance methods that can be used to examine or manipulate arrays; we'll discuss a few of the more commonly used methods here. A complete description of all of the Array class methods can be found in the FCL Reference on the MSDN web site.

The first three methods are static methods, meaning that they must be invoked on the Array class as a whole, passing in the array instance to be affected as an argument:

  • public static void Clear(Array array, int startIndex, int length): Resets the contents of all or part of an array to zero-equivalent values. The arguments include the array to be cleared, the starting index (counting from 0), and the number of elements to clear.

            int[] x = new int[5];
    
            // Clear the entire array.
            Array.Clear(x, 0, x.Length);
    
  • public static void Reverse(Array array): Reverses the order of the elements of a one-dimensional array.

            int[] x = {1, 2, 3};
            Array.Reverse(x);
            // x now contains the values: {3, 2, 1}
    

    There is also an overloaded version of this method in which the starting index and number of elements to reverse can be specified:

          public static void Reverse(Array array, int startIndex, int length)
    
  • public static void Sort(Array array): Sorts the elements of a one-dimensional array. The default sorting criterion is alphabetically, for string elements; or smallest-to-largest, for numerical elements.

                string[] names = {"Vijay", "Tiger", "Phil"};
                Array.Sort(names);
                // the order of the elements is now "Phil", "Tiger", "Vijay"
    

    There is also an overloaded version of this method in which the starting index and number of elements to sort can be specified:

          public static void Sort(Array array, int startIndex, int length)
    
Note?/td>

Yet another overloaded version of this method enables user-defined sorting criteria to be specified for arbitrary object types, but is beyond the scope of this book to address.

The next three methods are instance methods, meaning that they can be invoked on an individual array instance directly:

  • public object GetValue(int index): Returns the element at the specified index of a one-dimensional array as a generic object. If we happen to know what the specific type of that object is, we can cast it:

          String[] names = {"Chris", "Sandi", "Brennan"};
          // Cast the returned object as a string.
    
          string name = (string) names.GetValue(0);
    

    or:

          int[] numbers = {11, 17, 85};
          // Cast the returned object as an int.
          string number = (int) numbers.GetValue(0);
    

    There are also overloaded versions of this method for returning an element of a 2- or 3-dimensional array:

          public object GetValue(int index1, int index2)
          public object GetValue(int index1, int index2, int index3)
    
  • public void SetValue(object value, int index): Changes the element at the specified index of a one-dimensional array.

    There are also overloaded versions for changing an element of a two- or three-dimensional array:

          public void SetValue(object value, int index1, int index2)
          public void SetValue(object value, int index1, int index2, int index3)
    
    Note?/td>

    Even though GetValue and SetValue methods are provided by the Array class, the indexer notation is easier and more convenient to use. The syntax

        string name = names[0];
    

    is easier to write and understand than the equivalent syntax

        string name = (string) names.GetValue(0);
    
  • public int GetLength(int dimension): Returns the length of the specified dimension in an array of any number of dimensions. For example, to obtain the length of the first dimension of a two-dimensional array, we would use GetLength(0). (For a one-dimensional array, we may simply use the Length property to determine the length of the array.)

The ArrayList Class

We first learned about the ArrayList class in Chapter 6. It represents a simple, dynamically resizable, one-dimensional ordered list that allows us to store a varying number of object references without having to worry about properly sizing the container in advance. It's the logical equivalent of a one-dimensional Array, but whose size is automatically increased or decreased as needed.

ArrayLists can contain elements of any type (since all types in C# represent objects) and indeed it's possible to mix-and-match types within an ArrayList if it makes sense to do so in a given application.

The ArrayList class is defined within the System.Collections namespace. To refer to the ArrayList class by its simple name, a using directive can be included at the top of the code:

using System.Collections;

or an ArrayList can be referred to by its fully qualified name:

System.Collections.ArrayList.

Creating an ArrayList

An ArrayList object can be instantiated by using one of three constructors provided by the ArrayList class.

  • The simplest form of constructor is the parameterless constructor:

          ArrayList coursesTaken = new ArrayList();
    

    This constructor will create an empty ArrayList with an initial default capacity of 16. When the number of elements in the ArrayList reaches the current capacity of the ArrayList, its capacity is automatically doubled.

  • The second form of ArrayList constructor takes an integer argument representing the initial capacity of the ArrayList; e.g.,

          ArrayList students = new ArrayList(400);
    

    We might use this form of constructor if we knew that we would be adding a large number of object references to the ArrayList. Starting with a capacity greater than the default of 16 can increase performance by reducing the number of times the ArrayList would need to resize itself as it grows.

  • The third constructor takes as its argument any proper C# collection object—that is, any object belonging to one of the classes that implements the ICollection interface, which as we mentioned earlier includes all of the System.Collections classes along with the System.Array class— and copies the contents of the passed-in collection to populate the newly constructed ArrayList:

          // Create an array of Course objects ...
          Course[] courses = new Course[3];
          courses[0] = new Course("Math 101");
          courses[1] = new Course("Physics 250");
          courses[2] = new Course("Management 283");
    
          // Intervening details omitted.
          // ... and, later in the program, use it to initialize
          // an ArrayList.
          ArrayList coursesTaken = new ArrayList(courses);
    

In the previous code snippet, we initialized an ArrayList with the contents of a Course array; note that both collections are now referencing the same objects, as illustrated conceptually in Figure 13-12.

Click To expand
Figure 13-12: An ArrayList can be initialized with the contents of another collection.

ArrayList Properties

The ArrayList class declares a number of properties that return information about the ArrayList.

  • The Capacity property is used to get or set the capacity of the ArrayList. (If we programmatically attempt to resize an ArrayList to be smaller than the number of items it holds at any given time, an ArgumentOutOfRangeException is thrown.)

  • The Count property returns the current number of elements actually contained in the ArrayList.

  • To access an element stored inside an ArrayList, the ArrayList class defines a special indexer property that allows us to get or set an element of an ArrayList just as we would access an element of a standard array—with an integer expression representing the desired index, surrounded by brackets. When the indexer returns an ArrayList element, it's returned as a generic object, and the element is then typically cast to its original type.

Here is an example illustrating all of these properties:

Course c1 = new Course("Math 101");
Course c2 = new Course("Physics 250");
Course c3 = new Course("Management 283");

// Using the ArrayList indexer property. (Note that it looks as
// if we are accessing an Array!)
coursesTaken[0] = c1;
coursesTaken[1] = c2;
coursesTaken[2] = c3;

// Access the Capacity and Count properties.
Console.WriteLine("capacity = " + coursesTaken.Capacity);
Console.WriteLine("no. of elements = " + coursesTaken.Count);

// Let's now pull the ArrayList elements back out, again
// using the indexer.
for (int i = 0; i < coursesTaken.Count; i++) {
  // Use the indexer to retrieve each element of the ArrayList.
  // Cast a generic object reference back into a Course reference.
  Course c = (Course) coursesTaken[i];
  // We now can invoke the methods of a Course on c.
  Console.WriteLine(c.Name);
}

The previous example highlights why it can be dangerous to mix and match types in an ArrayList. The ArrayList elements are accessed one-by-one and cast into a Course object. The Name property is then invoked on each Course object. This process works fine if all of the ArrayList elements are Course objects (or objects that derive from Course). If there happened to be an element of a disparate type— say, a double—mixed in with the Course objects, the cast wouldn't work—an InvalidCastException would be thrown because there is no way to cast a double into a Course. We'll see a more complete example of how mixing types can be a problem in the "Collections and Casting" section later in the chapter.

ArrayList Methods

Some of the more commonly used ArrayList class methods are as follows:

  • public virtual int Add(object value): Adds an object reference to the end of the ArrayList by default, automatically expanding the ArrayList if need be to accommodate the reference. The provision of the virtual keyword allows derived classes to override this method as desired. The return value of the method is an int representing the index position in the ArrayList at which the argument was placed.

  • public void Add(ICollection c): Appends the contents of the specified collection to the end of the ArrayList.

  • public void SetRange(int startIndex, ICollection c): Replaces the current elements of an ArrayList with the contents of a different ICollection object, beginning at the specified starting index in the target ArrayList. If "c" contains more elements than the segment of the ArrayList that it is replacing, the ArrayList automatically gets extended in size as needed.

  • public IEnumerator GetEnumerator(): Retrieves an instance of a special type of object called an IEnumerator that can be used to iterate through the elements of an ArrayList. We'll discuss IEnumerators later in this chapter.

  • public int IndexOf(object obj): Hunts for the existence of a specific object reference and, if found, returns an integer indicating the (first) index location at which this reference was found (counting from 0). If the specified object isn't found, the value –1 is returned.

  • public bool Contains(object obj): Hunts for existence of the object reference in question and, if found, returns the value true, otherwise false.

  • public void RemoveAt(int index): Takes out the element at the specified index and "closes up"/"collapses" the resultant "hole." The value of the Count property decreases by one, but the Capacity remains the same. An ArgumentOutOfRangeException is thrown if the specified index exceeds the Count or is less than zero.

  • public void Remove(object obj): Hunts for existence of a specific object reference in question and, if found, removes the (first) occurrence of that reference from the ArrayList, again closing up the "hole." The Count decreases by one, but the Capacity remains the same. If the specified object isn't found, the ArrayList is unchanged. To remove all instances of obj, use a combination of the Contains and Remove methods in conjunction with a while loop:

          while (arraylist.Contains(x)) {
            arraylist.Remove(x)
          }
    
  • public void Sort(): Sorts the elements of the ArrayList. The default is to sort string elements alphabetically and numerical elements smallest-to-largest.

    Note?/td>

    There is an overloaded version of this method that enables us to customize the sorting algorithm for arbitrary object types, but discussing user-defined sorting algorithms is beyond the scope of this book.

  • public void Clear(): Empties out the ArrayList. The Count property is set to zero. The Capacity is unchanged.

  • public object[] ToArray(): Creates an instance of an object array, and copies the elements of the ArrayList into the array.

and there are more! Please consult the FCL Reference on the MSDN web site for a complete description of all of the ArrayList methods.

The Hashtable Class

The Hashtable class provides us with another way to manage collections of object references in C#. A Hashtable is a bit more sophisticated than an ArrayList because it gives us direct access to a given object based on a unique key value; it's an implementation of the dictionary collection type that we defined in Chapter 6. Both the key and the object itself can be of any object type; keys are often, but not always, strings.

Like the ArrayList class, the Hashtable class can be found in the System.Collections namespace. To refer to a Hashtable by its simple name, a using System.Collections; directive can be placed at the top of a source code file, or a Hashtable can be referred to by its fully qualified name— System.Collections.Hashtable.

Creating a Hashtable

A Hashtable object can be created using any one of the various constructors declared by the Hashtable class. The simplest way to instantiate a Hashtable is with the parameterless constructor, which creates an empty Hashtable. We then insert objects as desired using the Add method, whose header is as follows:

  public void Add(object key, object value)

Note that we must specify a key value for each item as we add it to the Hashtable, to be used to retrieve the item later on; we'll use simple string objects for the keys in this example—in particular, the students' social security numbers:

// Create a Hashtable instance (the "egg carton").
Hashtable students = new Hashtable();

// Create several Student objects ("eggs").
Student s1 = new Student("123-45-6789", "John Smith");
Student s2 = new Student("987-65-4321", "Mary Jones");
Student s3 = new Student("654-32-1987", "Jim Green");

// Store their handles in the Hashtable, using the value of the Ssn property
// (which happens to be declared as type string) as the key for each.
students.Add(s1.Ssn, s1);
students.Add(s2.Ssn, s2);
students.Add(s3.Ssn, s3);

The Hashtable class defines an indexer that can be used to get or set an element of the Hashtable. As with the ArrayList, the indexer is denoted by a pair of brackets; with Hashtables, however, the brackets surround the key value of the element to be accessed:

// Note that we have to recast the object when we retrieve it,
// just as we must do with ArrayLists.
Student s = (Student) students["123-45-6789"];  // retrieves the object
                                                // reference representing
                                                // Student John Smith
Console.WriteLine("name is " + s.Name);

or, by way of another example:

// Pseudocode.
string id = retrieve student ID number from a GUI;
Student s = (Student) students[id];

Note that we have to cast an object when we retrieve it from a Hashtable if we wish to manipulate it as an instance of its original type (in this case, a Student), just as we do when retrieving an object from an ArrayList.

Hashtable Properties

In addition to Count and indexer properties, which work the same for a Hashtable as they do for an ArrayList, the Hashtable class declares a number of other properties to return information about the Hashtable.

  • The Keys property returns an ICollection object whose elements are the keys contained in the Hashtable.

  • The Values property returns an ICollection object whose elements are the keys contained in the Hashtable.

Note?/td>

We'll learn how to iterate through generic ICollection collections later in this chapter.

and there are others. For a complete list of all Hashtable properties, consult the FCL reference on the MSDN web site.

Hashtable Methods

Some of the more commonly used methods from the Hashtable class are as follows:

  • public void Add(object key, object value): As already discussed, this method inserts the second object reference (value) into the table with a retrieval key represented by the first object reference (key). This method will throw an ArgumentException if there already was a previously stored object at that key location, so it's important to first verify that the key we're about to use isn't already in use in the table, using the ContainsKey method discussed next.

  • public bool ContainsKey(object key): Returns true if an entry with the designated key value is found in the Hashtable, otherwise returns false.

    Here's how we'd use the ContainsKey method in concert with the Add method to make sure we weren't trying to overwrite an existing entry in the Hashtable:

          Student s = new Student("111-11-1111", "Arnold Brown");
          if (students.ContainsKey(s.Ssn)) {
            // Whoops! This is a duplicate; we need to decide what to do!
            // details omitted ...
          }
          else {
            students.Add(s.Ssn, s); // OK, because no duplicate was detected.
          }
    
  • public bool ContainsValue(object value): Looks for the designated value without the aid of its associated key, and returns true if the value is found, otherwise returns false.

  • public void Remove(object key): Removes the reference to the object represented by the given key from the Hashtable; note that the object itself, as it exists outside of the Hashtable, is unaffected.

  • public IDictionaryEnumerator GetEnumerator(): Returns an IDictionaryEnumerator object that can be used to examine the key-object pairs currently stored in the Hashtable. We'll talk about enumerators in the next section.

  • public void Clear(): Empties out the Hashtable, as if it had just been newly instantiated. The Count property is set to zero.

Collections and Casting

As we learned in Chapter 6, the only C# collection type that allows us to constrain the types of objects that we insert is the Array. That is, when we declare an Array, we must declare the type of entities that we plan on inserting:

int[] x = new int[20]; // We're constraining x to hold int(eger)s.

With all other formal collection types in C#, however, we can't constrain the type of objects to be inserted when we declare the collection; as mentioned in Chapter 6, C# collections other than Arrays are designed to hold generic objects. We must therefore exercise programming discipline when inserting objects into such a collection to ensure that they all speak a "common language" in terms of messages that they understand if we wish to take advantage of polymorphism, because the C# compiler won't stop us from putting an odd assortment of objects into a collection, but the C# compiler or runtime may complain if we try to operate on the objects "inappropriately" after taking them back out.

To illustrate this notion, let's look at a simple example. We'll use an ArrayList as the collection type, but the concepts presented in this section apply to all of the C# System.Collections classes. Suppose we define a Student class that, among other things, declares a CalculateGPA method:

public class Student
{
  // Details omitted.

  public void CalculateGPA() {
    // Details omitted.
  }
}

Let's also declare a Professor class that does not declare a CalculateGPA method:

public class Professor {
  // Details omitted -- but, NO CalculateGPA method is declared.
}

We're permitted by the compiler to add an assortment of Student and Professor objects to the same ArrayList, because as we learned in Chapter 6, non-Array collections can't be constrained as to the type of object that they hold:

Student s = new Student();
Student t = new Student();
Professor p = new Professor();
Professor q = new Professor();
// We cannot declare an ArrayList as holding a particular type
// of object.
ArrayList people = new ArrayList();

// Add a mixture of Professors and Students to the collection.
people.Add(s);
people.Add(p);
people.Add(q);
people.Add(t);

As the programmers who wrote this code, we know that the zeroeth element of the ArrayList is a Student, and so let's use the indexer to retrieve that element and invoke the CalculateGPA method on it:

people[0].CalculateGPA();

This seems reasonable to do; however, when we try to compile the previous line of code, an error is generated:

Error CS0117: 'object' does not contain a definition for 'CalculateGPA'

Even though the zeroeth element of the ArrayList is indeed a Student object at run time, the compiler "knows" that an indexer returns generic object references. Since people[0] therefore is a generic object in the compiler's "eyes," and since the Object class doesn't declare a CalculateGPA method—only the Student class does—this line of code won't compile.

The solution to this compilation problem would be to cast the return value from the ArrayList indexer back into a Student before invoking the CalculateGPA method:

// Note addition of cast.
Student s2 = (Student) people[0];
s2.CalculateGPA();

or, collapsing this into a single line of code by chaining expressions:

 ((Student) people[0]).CalculateGPA();

Either way, the compiler will now be "happy," because by casting, we're effectively asking the compiler to "trust us" that, at run time, the object reference in question will indeed be referencing a Student object; and, since the compiler knows that the Student class defines a CalculateGPA method, all is well!

There is still one potential issue, however: if the ArrayList collection indeed contains a mixture of Student and Professor object references, and we want to iterate through the entire collection, we can't cast them as Students so as to call the CalculateGPA method because, at run time, some of the objects in the collection will be Professors for which CalculateGPA is undefined:

for (int i = 0; i < people.Count; i++) {
  Student x = (Student) people[i]; // The ith object may NOT be a Student!
  x.CalculateGPA();
}

The previous code snippet will compile, because once again we're asking the compiler to "trust us" that the objects in this collection will all be Student objects; but at run time, when we actually try to execute this code, an InvalidCastException will be thrown and the program will terminate as soon as the first Professor reference is encountered, because there is no valid way to cast a Professor object into a Student object.

If we want to iterate through our people collection using a looping construct, our only choices, having mixed Professors and Students into the same collection, are to

  • Treat them as generic Objects.

  • Cast them into a common supertype.

Let's look at the implications of these two choices.

If we treat all the objects in our people collection as generic objects, then the only methods that the compiler will allow us to call on each object reference are the handful of methods defined by the Object class:

for (int i = 0; i < people.Count; i++) {
  // We're NOT casting the references.
  object x = people[i];

  // Therefore, the only methods we can invoke on x
  // are methods defined by the Object class.
  Console.WriteLine(x.ToString());
}

However, if the various types in question all have some common supertype other than object—say, for example, that both Student and Professor derive from a common base class called Person—then we may cast all of the object references in the people collection to Person object references, since by virtue of the "is a" nature of inheritance, a Student is a Person, and a Professor is a Person. Under these circumstances, we may call any methods on the objects that are common to the Person class:


for (int i = 0; i < people.Count; i++) {
  Person x = (Person) people[i]; // Casting to a common supertype.
  x.printAddress(); // A method defined in the Person class.
}

Because a System.Collections collection such as an ArrayList or Hashtable treats everything that is inserted as a generic object reference, we as programmers must remember what class of object reference we're storing in a given collection so that we may cast these references back to the correct (common base) class when we retrieve them. One way to help remember what types are going to be stored in such a collection is to add a comment to the collection declaration to help us remember what sort of object references we intend to store within the collection:

ArrayList coursesTaken; // of Course object references

Stepping Through Collections with Enumerators

When we create collections of objects, we often have a need to step through the entire collection to process it in some fashion; for example, we may want to iterate through an ArrayList of Student object references to compute final grades for the semester. One way that we've already seen for stepping through an ArrayList is to use a for loop:

ArrayList enrollment = new ArrayList(); // of Student references

// Populate the ArrayList ... details omitted.

// Step through the ArrayList, and process all Students' grades.
for (int i = 0; i < enrollment.Count; i++) {
  Student s = (Student) enrollment[i];
  s.ComputeFinalGrade();
}

Processing all items in an ArrayList in this fashion is possible because we have a means for referring to a particular element in the ArrayList by its index: e.g., enrollment[i].

There is an alternative way of stepping through all of the elements in a System.Collections collection using a special type of object called an enumerator. An enumerator is a mechanism for iterating through a collection. When we obtain an enumerator for a collection—say, for an ArrayList—we can think of this conceptually as making a copy of the original collection's contents, similar in concept to making a photocopy of someone's address book (a collection of Person references) in preparation for conducting a telemarketing campaign (see Figure 13-13).

Click To expand
Figure 13-13: An enumerator can step through the elements of a collection.

We then step through the "photocopy" (enumerator) one by one, "crossing off each phone number" (processing each object reference) until we reach the end of the enumeration.

The IEnumerator Interface

As mentioned previously, the ArrayList class (and some of the other System.Collections classes, as well) defines a GetEnumerator method with the following header:

public virtual IEnumerator GetEnumerator()

The return type of this method, IEnumerator, is an interface defined in the System.Collections namespace. An IEnumerator object represents a temporary copy of the ArrayList for which it has been acquired, and has the following methods:

  • bool MoveNext(): Advances the enumerator to the next element in the collection. The method returns true if another element is successfully reached, or false if the end of the collection has been reached.

  • void Reset(): Moves the enumerator back to its original position before the first item in the collection, such that the next call to MoveNext will advance the cursor to the first element in the collection.

The IEnumerator interface also declares a read-only property (i.e., one that has a get but not a set accessor) named Current that accesses the element currently being pointed to by the cursor as a generic object reference.

Here's a simple example to illustrate how to put all of this IEnumerator functionality to use. We'll use this simple version of the Course class:

public class Course
{
  // Field.
  private string name;

  // Constructor.
  public Course(string n) {
    name = n;
  }

  // Property.
  public string Name {
    // Accessor details omitted.
  }
}

This next program will serve as the driver:

using System.Collections;

public class EnumDemo
{
  static void Main() {
    ArrayList coursesTaken = new ArrayList();

    Course c1 = new Course("Math 101");
    Course c2 = new Course("Physics 250");
    Course c3 = new Course("Management 283");

    // Insert Course object references into the ArrayList.
    coursesTaken.Add(c1);
    coursesTaken.Add(c2);
    coursesTaken.Add(c3);

    // Obtain an enumerator for the ArrayList.
    IEnumerator enum = coursesTaken.GetEnumerator();

    // Iterate through the collection using the enumerator.
    while (enum.MoveNext()) {
      // Use the Current property to retrieve the current element;
      // cast a generic object reference back into a Course reference.
      Course c = (Course) enum.Current;
      // We now can manipulate c as a Course object.
      Console.WriteLine(c.Name);
    }
  }
}

This program, when run, produces the following output:

Math 101
Physics 250
Management 283

The IDictionaryEnumerator Interface

As we now know, a Hashtable is a collection that stores data in the form of key-value pairs. A specialized type of enumerator is used with Hashtables —specifically, an IDictionaryEnumerator. As mentioned previously, the Hashtable class declares the following method to retrieve an IDictionaryEnumerator for a given Hashtable:

public virtual IDictionaryEnumerator GetEnumerator()

The IDictionaryEnumerator interface derives from IEnumerator and inherits the MoveNext and Reset methods, which were previously discussed. The IDictionaryEnumerator interface declares several additional properties; of particular interest are Key and Value, which can be used to return the key and value of the current element of the enumeration, respectively.

Here is an example of using an IDictionaryEnumerator to iterate through the elements of a Hashtable. We'll start with a simplified version of the Student class:

public class Student
{
  private string name;
  private string ssn;

  // Constructor.
  public Student(string s, string n) {
    ssn = s;
    name = n;
  }

  // Properties.
  public string Name {
    // Accessor details omitted.
  }
  public string Ssn {
    // Accessor details omitted.
  }
}

Here is the driver code for our IDictionaryEnumerator example:

using System;
using System.Collections;

public class EnumDemo2
{
  static void Main() {
    // Create an instance of a Hashtable.
    Hashtable students = new Hashtable();

    // Instantiate several Student objects.
    Student s1 = new Student("123-45-6789", "Chris Williams");
    Student s2 = new Student("987-65-4321", "Yukio Koyari");
    Student s3 = new Student("654-32-1987", "Maria Lopez");

    // Store their handles in the Hashtable, using the Ssn value as the key.
    students.Add(s1.Ssn, s1);
    students.Add(s2.Ssn, s2);
    students.Add(s3.Ssn, s3);

    // Obtain an IDictionaryEnumerator for the Hashtable.
    IDictionaryEnumerator enum = students.GetEnumerator();

    // Iterate through the collection using the IDictionaryEnumerator.
    while (enum.MoveNext()) {
      // Obtain the next Student object from the collection as the VALUE in
      // a key-value pair.
      Student s = (Student) enum.Value;
      Console.WriteLine(s.Name + ": " + s.Ssn);
    }
  }
}

When this program is run, the following output is produced:

Maria Lopez: 654-32-1987
Yukio Koyari: 987-65-4321
Chris Williams: 123-45-6789

Note that the student information has been printed out in a different order from the order in which the students were originally added to the Hashtable. Entries in a Hashtable aren't retrieved in any particular order by an IDictionaryEnumerator; we're simply guaranteed that we'll have iterated through all the items.

Iterating Through Generic ICollections

Recall that when we were talking earlier about Hashtables, we mentioned that they have two properties—Keys and Values—of type ICollection. Let's look at a way to iterate through the elements of a generic ICollection.

An ICollection object only has access to one method named CopyTo that has the following header:

void CopyTo(Array array, int index)

The CopyTo method is used to copy the contents of an ICollection into an Array object. If you recall from Chapter 6, the Array class is the parent of all C# arrays. The index parameter is the index of the array at which to begin copying the ICollection elements. Once the keys and/or values are transferred into an Array, the elements of the array can be accessed in the normal fashion.

As an example of iterating through the keys and values of a Hashtable, we'll use the same Student class as defined previously, but different driver code:

using System;
using System.Collections;

public class EnumDemo3
{
  static voi