[ Team LiB ] Previous Section Next Section

9.3 Indexers

There are times when it is desirable to access a collection within a class as though the class itself were an array. For example, suppose you create a listbox control named myListBox that contains a list of strings stored in a one-dimensional array, a private member variable named myStrings. A listbox control contains member properties and methods in addition to its array of strings. However, it would be convenient to be able to access the listbox array with an index, just as if the listbox were an array. For example, such a property would permit statements such as the following:

string theFirstString = myListBox[0];
string theLastString = myListBox[Length-1];

An indexer is a C# construct that allows you to access collections contained by a class using the familiar [] syntax of arrays. An indexer is a special kind of property and includes get( ) and set( ) methods to specify its behavior.

You declare an indexer property within a class using the following syntax:

type  this  [type argument ]{get; set;}

The return type determines the type of object that will be returned by the indexer, while the type argument specifies what kind of argument will be used to index into the collection that contains the target objects. Although it is common to use integers as index values, you can index a collection on other types as well, including strings. You can even provide an indexer with multiple parameters to create a multidimensional array!

The this keyword is a reference to the object in which the indexer appears. As with a normal property, you also must define get( ) and set( ) methods, which determine how the requested object is retrieved from or assigned to its collection.

Example 9-9 declares a listbox control (ListBoxTest) that contains a simple array (myStrings) and a simple indexer for accessing its contents.

C++ programmers take note: The indexer serves much the same purpose as overloading the C++ index operator ([]). The index operator cannot be overloaded in C#, which provides the indexer in its place.

Example 9-9. Using a simple indexer
namespace Programming_CSharp
{
   using System;
  
   // a simplified ListBox control
   public class ListBoxTest
   {
      private string[] strings;
      private int ctr = 0;

      // initialize the list box with strings
      public ListBoxTest(params string[] initialStrings)
      {
         // allocate space for the strings
         strings = new String[256]; 

         // 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 list box
      public void Add(string theString)
      {
         if (ctr >= strings.Length)
         {
            // handle bad index
         }
         else
            strings[ctr++] = theString;
      }

      // allow array-like access
      
      public string this[int index]
      {
         get
         {
            if (index < 0 || index >= strings.Length)
            {
               // handle bad index
            }
            return strings[index];
         }
         set
         {
            // add only through the add method
            if (index >= ctr )
            {
               // handle error
            }
            else
               strings[index] = value;
         }
      }
   
      // publish how many strings you hold
      public int GetNumEntries( )
      {
         return ctr;
      }
   }
    
   public class Tester
   {
      static void Main( )
      {
         // create a new list box and initialize
         ListBoxTest lbt = 
            new ListBoxTest("Hello", "World");

         // add a few strings
         lbt.Add("Who");
         lbt.Add("Is");
         lbt.Add("John");
         lbt.Add("Galt");

         // test the access
         string subst = "Universe";
         lbt[1] = subst;

         // access all the strings
         for (int i = 0;i<lbt.GetNumEntries( );i++)
         {
            Console.WriteLine("lbt[{0}]: {1}",i,lbt[i]);
         }
      }
   }
}

Output:
lbt[0]: Hello
lbt[1]: Universe
lbt[2]: Who
lbt[3]: Is
lbt[4]: John
lbt[5]: Galt

To keep Example 9-9 simple, strip the listbox control down to the few features we care about. The listing ignores everything having to do with being a user control and focuses only on the list of strings the listbox maintains and methods for manipulating them. In a real application, of course, these are a small fraction of the total methods of a listbox, whose principal job is to display the strings and enable user choice.

The first things to notice are the two private members:

private string[] strings;
private int ctr = 0;

In this program, the listbox maintains a simple array of strings: strings. Again, in a real listbox you might use a more complex and dynamic container, such as a hash table (described later in this chapter). The member variable ctr will keep track of how many strings have been added to this array.

Initialize the array in the constructor with the statement:

strings = new String[256]; 

The remainder of the constructor adds the parameters to the array. Again, for simplicity, add new strings to the array in the order received.

Because you cannot know how many strings will be added, use the keyword params, as described earlier in this chapter.

The Add( ) method of ListBoxTest does nothing more than append a new string to the internal array.

The key method of ListBoxTest, however, is the indexer. An indexer is unnamed, so use the this keyword:

public string this[int index]

The syntax of the indexer is very similar to that for properties. There is either a get( ) method, a set( ) method, or both. In the case shown, the get( ) method endeavors to implement rudimentary bounds checking, and assuming the index requested is acceptable, it returns the value requested:

get
{
    if (index < 0 || index >= strings.Length)
    {
       // handle bad index
    }
    return strings[index];
}

The set( ) method checks to make sure that the index you are setting already has a value in the listbox. If not, it treats the set as an error. (New elements can only be added using Add with this approach). The set accessor takes advantage of the implicit parameter value that represents whatever is assigned using the index operator:

set
{
if (index >= ctr )
 {
    // handle error
 }
 else
    strings[index] = value;
}

Thus, if you write:

lbt[5] = "Hello World"

the compiler will call the indexer set( ) method on your object and pass in the string Hello World as an implicit parameter named value.

9.3.1 Indexers and Assignment

In Example 9-9, you cannot assign to an index that does not have a value. Thus, if you write:

lbt[10] = "wow!";

you would trigger the error handler in the set( ) method, which would note that the index you've passed in (10) is larger than the counter (6).

Of course, you can use the set( ) method for assignment; you simply have to handle the indexes you receive. To do so, you might change the set( ) method to check the Length of the buffer rather than the current value of counter. If a value was entered for an index that did not yet have a value, you would update ctr:

set
{
   // add only through the add method
   if (index >= strings.Length )
   {
      // handle error
   }
   else
   {
      strings[index] = value;
      if (ctr < index+1)
         ctr = index+1;
   }
}

This allows you to create a "sparse" array in which you can assign to offset 10 without ever having assigned to offset 9. Thus, if you now write:

lbt[10] = "wow!";

the output would be:

lbt[0]: Hello
lbt[1]: Universe
lbt[2]: Who
lbt[3]: Is
lbt[4]: John
lbt[5]: Galt
lbt[6]:
lbt[7]:
lbt[8]:
lbt[9]:
lbt[10]: wow!

In Main( ), you create an instance of the ListBoxTest class named lbt and pass in two strings as parameters:

ListBoxTest lbt = new ListBoxTest("Hello", "World");

Then call Add( ) to add four more strings:

// add a few strings
lbt.Add("Who");
lbt.Add("Is");
lbt.Add("John");
lbt.Add("Galt");

Before examining the values, modify the second value (at index 1):

string subst = "Universe";
lbt[1] = subst;

Finally, display each value in a loop:

for (int i = 0;i<lbt.GetNumEntries( );i++)
{
    Console.WriteLine("lbt[{0}]: {1}",i,lbt[i]);
}

9.3.2 Indexing on Other Values

C# does not require that you always use an integer value as the index to a collection. When you create a custom collection class and create your indexer, you are free to create indexers that index on strings and other types. In fact, the index value can be overloaded so that a given collection can be indexed, for example, by an integer value or by a string value, depending on the needs of the client.

In the case of our listbox, we might want to be able to index into the listbox based on a string. Example 9-10 illustrates a string index. The indexer calls findString( ), which is a helper method that returns a record based on the value of the string provided. Notice that the overloaded indexer and the indexer from Example 9-9 are able to coexist.

Example 9-10. Overloading an index
namespace Programming_CSharp
{
   using System;
  
   // a simplified ListBox control
   public class ListBoxTest
   {
      private string[] strings;
      private int ctr = 0;

      // initialize the list box with strings
      public ListBoxTest(params string[] initialStrings)
      {
         // allocate space for the strings
         strings = new String[256]; 

         // 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 list box
      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;
         }
      }
   
      private int findString(string searchString)
      {
         for (int i = 0;i<strings.Length;i++)
         {
            if (strings[i].StartsWith(searchString))
            {
               return i;
            }
         }
         return -1;
      }
      
      // index on string
      public string this[string index]
      {
         get
         {
            if (index.Length == 0)
            {
               // handle bad index
            }

            return this[findString(index)];
         }
         set
         {
            strings[findString(index)] = value;
         }
      }



      // publish how many strings you hold
      public int GetNumEntries( )
      {
         return ctr;
      }
   }

   public class Tester
   {        
      static void Main( )
      {
         // create a new list box and initialize
         ListBoxTest lbt = 
            new ListBoxTest("Hello", "World");

         // add a few strings
         lbt.Add("Who");
         lbt.Add("Is");
         lbt.Add("John");
         lbt.Add("Galt");

         // test the access
         string subst = "Universe";
         lbt[1] = subst;
         lbt["Hel"] = "GoodBye";
         // lbt["xyz"] = "oops";

         // access all the strings
         for (int i = 0;i<lbt.GetNumEntries( );i++)
         {
            Console.WriteLine("lbt[{0}]: {1}",i,lbt[i]);
         }      // end for
      }         // end main
   }            // end tester
}               // end namespace

Output:
lbt[0]: GoodBye
lbt[1]: Universe
lbt[2]: Who
lbt[3]: Is
lbt[4]: John
lbt[5]: Galt

Example 9-10 is identical to Example 9-9 except for the addition of an overloaded indexer, which can match a string, and the method findString, created to support that index.

The findString method simply iterates through the strings held in myStrings until it finds a string that starts with the target string we use in the index. If found, it returns the index of that string; otherwise it returns the value -1.

We see in Main( ) that the user passes in a string segment to the index, just as with an integer:

lbt["Hel"] = "GoodBye";

This calls the overloaded index, which does some rudimentary error-checking (in this case, making sure the string passed in has at least one letter) and then passes the value (Hel) to findString. It gets back an index and uses that index to index into myStrings:

return this[findString(index)];

The set value works in the same way:

myStrings[findString(index)] = value;

The careful reader will note that if the string does not match, a value of -1 is returned, which is then used as an index into myStrings. This action then generates an exception (System.NullReferenceException), as you can see by uncommenting the following line in Main( ):

lbt["xyz"] = "oops";

The proper handling of not finding a string is, as they say, left as an exercise for the reader. You might consider displaying an error message or otherwise allowing the user to recover from the error.

    [ Team LiB ] Previous Section Next Section