Team LiB   Previous Section   Next Section

15.4 Indexers

Some classes act like collections. For example, a ListBox class might act as a collection of the strings it displays. You might write your own School class that would act as a collection of all the Students enrolled in the school.

An indexer is a C# construct that allows you to treat a class as if it were a collection, using the familiar [] syntax of arrays. This lets you write:

mySchool[5];

to obtain the sixth Student in your School object.

An indexer is actually a special kind of property that includes get() and set() methods to specify its behavior. Declare an indexer property within a class using the following syntax:

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

The type specifies the type of object that returns the indexer, while the type argument within the square brackets specifies what kind of argument is used to index into the collection that contains the target objects.

The square brackets do not indicate that type argument is optional. The square brackets themselves are part of the syntax of the indexer.

Although it is common to use integers as index values, you can index a collection on other types as well, including strings. This lets you write:

mySchool["John Galt"];

to index into your School's internal collection and find the Student object whose name field (for example) matches the index string.

You can even provide an indexer with multiple parameters if you wish to provide the syntax for accessing into your class as if it were a multidimensional array!

The this keyword is a reference to the current object and stands in as the name of the indexer. As with a normal property, you must define get() and set() methods that determine how the requested object is retrieved from or assigned to its collection.

Let's look at an example of how you might use an indexer. 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 object with an index, just as if the listbox itself were an array. For example, such a property would permit statements like the following:

string theFirstString = myListBox[0];
string theLastString = myListBox[LastElementOffset];

Example 15-10 declares a listbox control class called ListBoxTest that contains, as a private member variable, a simple array (myStrings). The listbox control also provides an indexer for accessing the contents of its internal array. To keep Example 15-10 simple, you'll strip the listbox control down to just a few features.

The example ignores everything having to do with listbox being a user control and focuses only on the array 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.

Example 15-10. Using an indexer
using System;

namespace Indexers
{
    // a simplified ListBox control
    public class ListBoxTest
    {
        private string[] strings;
        private int ctr = 0;

        // initialize the listbox 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 listbox
        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;
        }

    }

   class Tester
   {
      public void Run()
      {
          // create a new listbox and initialize
          ListBoxTest lbt = 
              new ListBoxTest("Hello", "World");

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

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

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


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

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

      [STAThread]
      static void Main()
      {
         Tester t = new Tester();
         t.Run();
      }
   }
}
 Output:
After creation...
lbt[0]: Hello
lbt[1]: World

After adding strings...
lbt[0]: Hello
lbt[1]: World
lbt[2]: Who
lbt[3]: Is
lbt[4]: John
lbt[5]: Galt

After editing strings...
lbt[0]: Hello
lbt[1]: Universe
lbt[2]: Who
lbt[3]: Is
lbt[4]: John
lbt[5]: Galt

Example 15-10 begins by creating two private members: an array of strings named (appropriately) strings and an integer named ctr (which is initialized to 0):

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

The member variable ctr keeps track of how many strings have been added to this array.

Initialize the array in the constructor with the following statement, which allots memory for 256 String objects:

strings = new String[256];

The remainder of the constructor adds the parameters to the array. Again, for simplicity, simply add new strings to the array in the order received in the constructor's parameter list:

// copy the strings passed in to the constructor
foreach (string s in initialStrings)
{
    strings[ctr++] = s;
}

Because you cannot know at compile time how many strings will be added, use the params keyword, 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 feature of ListBoxTest, however, is the indexer. Declare the indexer property with the this keyword:

public string this[int index]

The syntax of the indexer is very similar to that for properties. There needs to be a get() method, a set() method, or both. In Example 15-10, 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 method takes advantage of the implicit parameter named 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 calls the indexer set() method on your object and passes in the string Hello World as an implicit parameter named value. This is analogous to how set accessors work for other properties. Remember, if you have a property named Weight and you write:

myObject.Weight = 5;

the set accessor is called and the implicit parameter value evaluates to 5. Similarly, if you write:

myObject[3] = "hello";

the indexer property's set accessor is called and the implicit parameter value evaluates to "hello." The offset (3) is available to the set accessor as the index. You can thus write:

strings[index] = value

and index will be 3 and value will be "hello," and this code will assign the string hello to the fourth element in the member variable strings.

15.4.1 Indexers and Assignment

You cannot assign to an index that does not have a value. Thus, in Example 15-10, if you write:

lbt[10] = "wow!";

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

As an alternative, you might change the set() method to check the Length of the buffer rather than the current value of the 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!

15.4.2 Indexing on Other Values

C# does not require that you always use an integer value as the index to a collection. 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 your listbox, you might want to be able to index into the listbox based on a string. Example 15-11 illustrates a string indexer. 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 15-10 are able to coexist. The complete listing is shown, followed by the output and then a detailed analysis.

Example 15-11. String indexer
using System;

namespace Indexers
{
    // a simplified ListBox control
    public class ListBoxTest
    {
        private string[] strings;
        private int ctr = 0;

        // initialize the listbox 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 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;
            }
        }
   
        // helper method, given a string find
        // first matching record that starts with the target
        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;
        }
    }


   class Tester
   {
      public void Run()
      {
          // create a new listbox 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]);
          }     
          

      }

      [STAThread]
      static void Main()
      {
         Tester t = new Tester();
         t.Run();
      }
   }
}
 Output:
lbt[0]: GoodBye
lbt[1]: Universe
lbt[2]: Who
lbt[3]: Is
lbt[4]: John
lbt[5]: Galt

Example 15-11 is identical to Example 15-10 except for the addition of an overloaded indexer that can match a string:

 public string this[string index]

To support the indexer that indexes on a string, you also create the method findString():

 private int findString(string searchString)

The findString() method simply iterates through the strings held in the private member array myStrings until it finds a string that starts with the target string used in the index. If found, it returns the index of that string, otherwise it returns the value -1.

You see in Main() that the user first passes in an integer as the index value:

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

The second time you index into the array, however, you pass in a string, rather than an integer:

lbt["Hel"] = "GoodBye";

This calls the overloaded indexer's set accessor:

public string this[string index]
{
    get
    {
        if (index.Length == 0)
        {
            // handle bad index
        }

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

The set accessor assigns the value ("Goodbye") to the strings array. It needs to know what index to use, so it must translate the index it was given ("hel") into an integer index for the strings array. To do so, it calls the helper method findString(). The findString() method takes the string passed in to the set accessor ("Hel") and returns the index of the matching string in the strings array, or it returns -1 if that string is not found.

The set accessor then uses that returned value as an index into the strings array. If findString() returns -1 (the string was not found), the index into strings generates an exception (System.NullReferenceException), as you can see by un-commenting 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.

Exceptions are discussed in Chapter 18.

    Team LiB   Previous Section   Next Section