Creating Your Own Collections

We've thus far looked at some of the most popular collection classes in this chapter. However, you can also build your own collections, and we'll see how to do that in the rest of this chapter.

SHOP TALK : ADDING A COLLECTION CLASS

In a production or team environment, it's often not enough to simply release a new and useful class for others to use. You'll also normally have to add documentation, as well as register versioning and tracking data in a central data store. In C#, it's also common to release a collection class for objects of your new classif you don't, it's one of the things people are sure to ask for. There are a number of interfaces to implement in order to build C# collections, and we'll take a look at them in this chapter.


Creating Indexers

The most common way to access the elements in a collection is using the [] operator, and in C#, you can support [] in your own collections by using an indexer .

For example, say we wanted to create a collection named CustomerList (technically speaking, it's not a true list collection because it won't implement all the properties and methods of the IList interface, which we'll see at the end of the chapter). We'll store a name for each customer in the list, allowing programmers to pass a list of strings to the CustomerList constructor. After we implement the indexer, you can access customer names like this: customerlist [ index ] . Here's what the constructor looks like. Note that we've allowed for a variable number of names passed to the constructor:

FOR C++ PROGRAMMERS

You cannot overload the [] operator in C# as you can in C++; you use indexers instead.


 
 public class CustomerList {   private string[] customers;   private int number = 0;   public CustomerList(params string[] customerNames)   {     customers = new string[100];     foreach (string name in customerNames)     {       customers[number++] = name;     }   }   .   .   . 

To write an indexer, you use the this keyword as the indexer's name. You also use square brackets for the single parameter passed to the indexer, which is the index value that the calling code wants to access. The rest of the indexer's code is similar to a property's code. You use get and set methods to get and set the value at the given index, using the value keyword as you do when writing a property:

 
 public string this[int index] {   get   {     return customers[index];   }   set   {     customers[index] = value;   } } 

That's all it takes to implement an indexer. You can see this code at work in ch06_10.cs, Listing 6.10. In this example, we're creating a CustomerList object, storing four customers in it, and then looping over them with a for loop that retrieves those names with the [] operator.

Listing 6.10 Creating a Collection Class (ch06_10.cs)
 public class ch06_10 {   static void Main()   {  CustomerList customerList = new CustomerList("Ralph", "Ed",   "Alice", "Trixie");   for (int loopIndex = 0; loopIndex < 4; loopIndex++)   {   System.Console.WriteLine("customerList[{0}] = {1}",   loopIndex, customerList[loopIndex]);   }   }  } public class CustomerList {   private string[] customers;   private int number = 0;   public CustomerList(params string[] customerNames)   {     customers = new string[100];     foreach (string name in customerNames)     {       customers[number++] = name;     }   }   public string this[int index]   {     get     {       return customers[index];     }     set     {       customers[index] = value;     }   } } 

Here's what you see when you run ch06_10:

 
 C:\>ch06_10 customerList[0] = Ralph customerList[1] = Ed customerList[2] = Alice customerList[3] = Trixie 

You don't need to index with integers; as with other collections, you can index on any data type, or even multiple types, if you overload an indexer. For example, ch06_11.cs (as presented in Listing 6.11) shows how to overload the indexer in the previous example for strings as well as integers, allowing you to access customers as customerlist ["zero"] , customerlist ["one"] , and so on.

WATCH THE VALUES PASSED TO INDEXERS

Example ch06_10.cs demonstrates how to write an indexer, but note that you should implement code in an indexer as carefully as you would for a property, making sure values you consider illegal are not stored. Even more important is making sure that the index value isn't outside the bounds of the array you're using to store data for the collection; if it is, C# will stop your application.


Listing 6.11 Indexing with Strings (ch06_11.cs)
 public class ch06_11 {   static void Main()   {     CustomerList customerList = new CustomerList("Ralph",       "Ed", "Alice", "Trixie");  System.Console.WriteLine("customerList[\"{0}\"] = {1}",   "zero", customerList["zero"]);   System.Console.WriteLine("customerList[\"{0}\"] = {1}",   "one", customerList["one"]);   System.Console.WriteLine("customerList[\"{0}\"] = {1}",   "two", customerList["two"]);   System.Console.WriteLine("customerList[\"{0}\"] = {1}",   "three", customerList["three"]);  } } public class CustomerList {   private string[] customers;   private int number = 0;   public CustomerList(params string[] customerNames)   {     customers = new string[100];     foreach (string name in customerNames)     {       customers[number++] = name;     }   }   public string this[int index]   {     get     {       return customers[index];     }     set     {       customers[index] = value;     }   }  public string this[string index]   {   get   {   switch (index)   {   case "zero":   return customers[0];   case "one":   return customers[1];   case "two":   return customers[2];   case "three":   return customers[3];   default:   return "Out of bounds.";   }   }   set   {   switch (index)   {   case "zero":   customers[0] = value;   break;   case "one":   customers[1] = value;   break;   case "two":   customers[2] = value;   break;   case "three":   customers[3] = value;   break;   }   }  } } 

Here's what you see when you run ch06_11, where we've indexed with strings:

 
 C:\>ch06_11 customerList["zero"] = Ralph customerList["one"] = Ed customerList["two"] = Alice customerList["three"] = Trixie 

Creating Enumerators

We've used a for loop to iterate over the members of our CustomerList collection. How about working with the foreach loop as other collections do? To do that, you must implement the IEnumerable interface (as other collections like arrays, array lists, queues, stacks do).

The IEnumerable interface is made up of a MoveNext method that moves to the next element in the collection, returning false if there are no more elements and true if there are. This interface also has a Current property that returns the current element in the collection, and a Reset method to make the first object in the collection the current object again. To add enumerator support to the CustomerList collection, you have to add a method named GetEnumerator , which will return an enumerator object that implements the IEnumerable interface.

That enumerator object will be of a new class we'll call CustomerListEnumerator , a private class nested in the CustomerList class. The MoveNext method of this object will return true if it can move to the next element in the customer list, and false otherwise . The Current property returns the current object, and the Reset method sets the index back to -1:

 
 private class CustomerListEnumerator : IEnumerator {   private CustomerList customerList;   private int index;   public CustomerListEnumerator(CustomerList customerList)   {     this.customerList = customerList;     index = -1;   }   public bool MoveNext()   {     if (index < customerList.number - 1){       index++;       return true;     }     else {       return false;     }   }   public object Current   {     get     {       return(customerList[index]);     }   }   public void Reset()   {     index = -1;   } } 

That's all you need to create an enumerator. To let the CustomerList class return an enumerator object, we need to add a method named GetEnumerator to CustomerList :

 
 public IEnumerator GetEnumerator() {   return (IEnumerator) new CustomerListEnumerator(this); } 

Now code can loop over the elements in a CustomerList collection using a foreach loop, as you see in ch06_12.cs, Listing 6.12.

Listing 6.12 Implementing IEnumerable (ch06_12.cs)
 using System.Collections; public class ch06_12 {   static void Main()   {     CustomerList customerList = new CustomerList("Ralph", "Ed",       "Alice", "Trixie");  foreach (string customer in customerList)   {   System.Console.WriteLine(customer);   }  } } public class CustomerList: IEnumerable {   private string[] customers;   private int number = 0;   public CustomerList(params string[] customerNames)   {     customers = new string[100];     foreach (string name in customerNames)     {       customers[number++] = name;     }   }   public string this[int index]   {     get     {       return customers[index];     }     set     {       customers[index] = value;     }   }  public IEnumerator GetEnumerator()   {   return (IEnumerator) new CustomerListEnumerator(this);   }   private class CustomerListEnumerator : IEnumerator   {   private CustomerList customerList;   private int index;   public CustomerListEnumerator(CustomerList customerList)   {   this.customerList = customerList;   index = -1;   }   public bool MoveNext()   {   if (index < customerList.number - 1){   index++;   return true;   }   else {   return false;   }   }   public object Current   {   get   {   return(customerList[index]);   }   }   public void Reset()   {   index = -1;   }   }  } 

Here's what you see when you run ch06_12:

 
 C:\>ch06_12 Ralph Ed Alice Trixie 

Note that you don't need a foreach loop to work with an enumerator; you can handle the enumerator yourself, directly. For example, here's how you might use an enumerator to perform the same work as the foreach loop in ch06_12.cs:

 
 System.Collections.IEnumerator enumerator = customerList.GetEnumerator(); while (enumerator.MoveNext()) {   System.Console.WriteLine(enumerator.Current); } 

You can also handle dictionary-based collections with IDictionaryEnumerator objects, which support both Key and Value properties. Here's an example:

 
 IDictionaryEnumerator enumerator = hashTable.GetEnumerator(); while (enumerator.MoveNext()) {   System.Console.WriteLine("key: {0} value:{1}",     enumerator.Key, enumerator.Value ); } 

Supporting Sorts and Comparisons

You can set up your collection to support comparisons and sorts by implementing the IComparable and IComparer interfaces. The IComparable interface lets you compare objects on an object-by-object basis, and the IComparer interface lets you customize how comparisons are performed between all objects in your collection. We'll take a look at the IComparable interface first.

Implementing IComparable

The IComparable interface supports the CompareTo method, which is built into every object in your collection. This method compares the current object to another object you pass to it, which lets you use this method for object-by-object comparisons. If you implement the IComparable interface and then use C# collections like arrays or array lists to build a collection of your objects, the collection's sorting routines, like Array. Sort , will know how to sort your objects. The IComparable interface's CompareTo method returns a value that is:

  • Less than zero if the current object is less than the passed object.

  • Zero if the current object is equal to the passed object.

  • Greater than zero if the current object is greater than the passed object.

Here's an example where we implement the IComparer interface n the Customer class. When we build a collection of these objects using an array and then sort that array, each object's CompareTo method will be called by Array.Sort . In this case, we'll just use the standard String class's CompareTo to implement our own CompareTo method, sorting on the customer's name:

 
  public class Customer : System.IComparable  {   private string name;   public Customer(string name)   {      this.name = name;   }   public override string ToString()   {      return name;   }  public int CompareTo(object obj)   {   Customer customer = (Customer) obj;   return this.name.CompareTo(customer.name);   }  } 

Now the Sort methods built into C# collections like Array and ArrayList will know how to sort Customer objects. You can see how this works in ch06_13.cs, Listing 6.13, where we create an array of Customer objects, each with their own built-in CompareTo method, and sort that array using Array.Sort .

Listing 6.13 Implementing IComparable (ch06_13.cs)
 using System.Collections; public class ch06_13 {   static void Main()   {  Customer[] customerArray = new Customer[4];   customerArray[0] = new Customer("Ralph");   customerArray[1] = new Customer("Ed");   customerArray[2] = new Customer("Alice");   customerArray[3] = new Customer("Trixie");   System.Console.WriteLine("The customers:");   for (int loopIndex = 0; loopIndex < customerArray.Length; loopIndex++)   {   System.Console.Write("{0} ", customerArray[loopIndex]);   }   System.Console.WriteLine();   System.Console.WriteLine();   System.Array.Sort(customerArray);   System.Console.WriteLine("The sorted customers:");   for (int loopIndex = 0; loopIndex < customerArray.Length; loopIndex++)   {   System.Console.Write("{0} ", customerArray[loopIndex]);   }   System.Console.WriteLine();  } } public class Customer : System.IComparable {   private string name;   public Customer(string name)   {      this.name = name;   }   public override string ToString()   {      return name;   }   public int CompareTo(object obj)   {      Customer customer = (Customer) obj;      return this.name.CompareTo(customer.name);   } } 

Here's what you see when you run ch06_13.cs. First you see the unsorted array of Customer objects, and then the sorted version:

 
 C:\>ch06_13 The customers: Ralph Ed Alice Trixie The sorted customers: Alice Ed Ralph Trixie 
Implementing IComparer

You can gain more control over how your objects are sorted in a C# collection like an array or array list by implementing the IComparer interface. This interface has one method, Compare , which is passed two objects to compare. You use the interface we just saw, IComparable , to implement object-by-object comparisons, because that interface's CompareTo method only compares the current object to an object passed to that method. On the other hand, the IComparer interface's Compare method is passed both objects to compare, which means you can customize the way sorts are performed in general.

Let's take a look at an example to make this clearer. Say that you wanted to store both a first name and a last name for each customer in Customer objects, and that you wanted to let code sort on the first name or the last name. If you implemented the IComparable interface, you'd have to pass data to each object letting it know whether to sort on the first or last name in those objects' CompareTo methods. On the other hand, if you create an object of the IComparer interface, you only need to tell that one object whether you're comparing first or last names, because both Customer objects to compare will be passed to you in the Compare method, and you can customize the sort for first or last names yourself.

To implement this in code, we expand the Customer class to store first and last names for each customer:

 
 public class Customer {   private string firstName;   private string lastName;   public Customer(string firstName, string lastName)   {      this.firstName = firstName;      this.lastName = lastName;   }   .   .   . 

Next, we add a new nested class to the Customer class named CustomerComparer , which implements the IComparer interface's Compare method. We'll add a new field named useFirstName to the CustomerComparer class; if this field is true , the sort will be on customers' first names, and on their last names otherwise. The Compare method is passed both Customer objects to compare, and we can perform the actual comparison with a new version of the CompareTo method in each Customer object. This new version will take not only the Customer object to compare the current object to, but also the useFirstName argument to determine whether it should compare first or last names:

 
 public class CustomerComparer : IComparer {   public bool useFirstName = true;   public int Compare(object object1, object object2)   {     Customer customer1 = (Customer) object1;     Customer customer2 = (Customer) object2;     return customer1.CompareTo(customer2, useFirstName);   } } 

The old version of the Customer class's CompareTo method only took the object to compare the current object to, but this version also accepts the useFirstName argument, which indicates whether the sort should be on the first or last names:

 
 public int CompareTo(object obj, bool useFirstName) {  Customer customer = (Customer) obj;   if (useFirstName){   return this.firstName.CompareTo(customer.firstName);   } else {   return this.lastName.CompareTo(customer.lastName);   }  } 

When you want to sort an array of Customer objects, you can pass the Array.Sort method the IComparer object to use. We use an object of the CustomerComparer class here. To get that object, we add a static method to the Customer class named GetComparer :

 
 public static CustomerComparer GetComparer() {   return new Customer.CustomerComparer(); } 

Now when you want to sort an array of Customer objects, you can call Customer.GetComparer to get a CustomerComparer object, customize that object's useFirstName field to true or false , and pass that object to the Array.Sort method, as you see in ch06_14.cs, Listing 6.14. In this way, you have to set only the useFirstName field of the single CustomerComparer object instead of customizing each Customer object for the type of sort you want to do.

Listing 6.14 Implementing IComparer (ch06_14.cs)
 using System.Collections; public class ch06_14 {   static void Main()   {     Customer[] customerArray = new Customer[4];  customerArray[0] = new Customer("Ralph", "Smith");   customerArray[1] = new Customer("Ed", "Franklin");   customerArray[2] = new Customer("Alice", "Johnson");   customerArray[3] = new Customer("Trixie", "Patterson");  System.Console.WriteLine("The customers:");     for (int loopIndex = 0; loopIndex < customerArray.Length; loopIndex++)     {       System.Console.Write("{0} ", customerArray[loopIndex]);     }     System.Console.WriteLine();     System.Console.WriteLine();  Customer.CustomerComparer comparer = Customer.GetComparer();   comparer.useFirstName = false;   System.Array.Sort(customerArray, comparer);  System.Console.WriteLine("The sorted customers:");     for (int loopIndex = 0; loopIndex < customerArray.Length; loopIndex++)     {       System.Console.Write("{0} ", customerArray[loopIndex]);     }     System.Console.WriteLine();   } } public class Customer {   private string firstName;   private string lastName;   public Customer(string firstName, string lastName)   {      this.firstName = firstName;      this.lastName = lastName;   }   public override string ToString()   {      return firstName + " " + lastName;   }   public int CompareTo(object obj, bool useFirstName)   {      Customer customer = (Customer) obj;      if (useFirstName){        return this.firstName.CompareTo(customer.firstName);      } else {        return this.lastName.CompareTo(customer.lastName);      }   }   public static CustomerComparer GetComparer()   {      return new Customer.CustomerComparer();   }   public class CustomerComparer : IComparer   {     public bool useFirstName = true;     public int Compare(object object1, object object2)     {       Customer customer1 = (Customer) object1;       Customer customer2 = (Customer) object2;       return customer1.CompareTo(customer2, useFirstName);     }   } } 

Creating a True Collection

If you want to build an "official" C# collection, you should also implement the ICollection interface, which all built-in C# collections implement. This collection has the following properties and methods:

  • The Count property returns the number of objects in the collection.

  • The IsSynchronized property is true if the collection is thread-safe (which we'll discuss in Chapter 15, "Using Multithreading and Remoting").

  • The SyncRoot property returns an object that threads can use to synchronize access to the collection (only when the collection is thread-safe). Thread synchronization is also discussed in Chapter 15.

  • The CopyTo method copies the collection to an array.

For example, you can pass the CopyTo method an array to copy the collection into, along with the zero-based index in the array at which copying begins:

 
 void CopyTo(Array  array  , int  index  ); 

Here's how you might implement that method in the CustomerList collection:

 
 public class CustomerList {   private string[] customers;   private int number = 0;     .     .     .  public void CopyTo(object[] array, int index)   {   if(index >= number){   array = null;   return;   }   for(int loopIndex = index; loopIndex < number; loopIndex++){   array[loopIndex - index] = customers[loopIndex];   }   }  

Now you can use the CopyTo method of the new CustomerList collection, which implements the ICollection interface, to copy the customer list into an array, as you see in ch06_15.cs, Listing 6.15.

Listing 6.15 Implementing ICollection (ch06_15.cs)
 public class ch06_15 {   static void Main()   {     CustomerList customerList = new CustomerList("Ralph", "Ed",       "Alice", "Trixie");     string[] array = new string[4];  customerList.CopyTo(array, 0);   for (int loopIndex = 0; loopIndex < array.Length; loopIndex++)   {   System.Console.WriteLine("array[{0}] = {1}",   loopIndex, array[loopIndex]);   }  } } public class CustomerList {   private string[] customers;   private int number = 0;   public CustomerList(params string[] customerNames)   {     customers = new string[100];     foreach (string name in customerNames)     {       customers[number++] = name;     }   }   public void CopyTo(object[] array, int index)   {     if(index >= number){       array = null;       return;     }     for(int loopIndex = index; loopIndex < number; loopIndex++){       array[loopIndex - index] = customers[loopIndex];     }   }   public int Count   {     get     {       return number;     }   }   public bool IsSynchronized   {     get     {       return true;     }   }   public object SyncRoot   {     get     {       return this;     }   }   public string this[int index]   {     get     {       return customers[index];     }     set     {       customers[index] = value;     }   } } 

Here are the results of ch06_15.cs, where the CustomerList collection, which now implements the ICollection interface, was converted to an array and displayed:

 
 C:\>ch06_15 array[0] = Ralph array[1] = Ed array[2] = Alice array[3] = Trixie 

Creating a List-Based Collection

The advantage of lists like ArrayList is that you can use the Add and Remove methods to add and remove elements at runtime. To create a list-based collection, you need to implement the IList interface, which has these properties:

  • IsFixedSize When implemented by a class, returns true if the list has a fixed size.

  • IsReadOnly When implemented by a class, returns true if the list is read-only.

  • Item When implemented by a class, gets or sets the element at the given index.

The IList interface also has these methods:

  • Add adds an item to the list.

  • Clear removes all items from the IList .

  • Contains determines whether the list contains a specific value.

  • IndexOf determines the index of a specific item in the list.

  • Insert inserts an item to the list at the given position.

  • Remove removes the first occurrence of a specific object from the list.

  • RemoveAt removes the list item at the given index.



Microsoft Visual C#. NET 2003 Kick Start
Microsoft Visual C#.NET 2003 Kick Start
ISBN: 0672325470
EAN: 2147483647
Year: 2002
Pages: 181

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