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 list box control named myListBox that contains a list of strings stored in a one-dimensional array, a private member variable named myStrings. A list box control contains member properties and methods in addition to its array of strings. However, it would be convenient to be able to access the list box array with an index, just as if the list box were an array. For example, such a property would permit statements like 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 that determine how the requested object is retrieved from or assigned to its collection.

Example 9-9 declares a list box control, ListBoxTest, which contains a simple array (myStrings) and a simple indexer for accessing its contents.

Tip

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
   {
      // 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;
      }

      private string[] strings;
      private int ctr = 0;
   }
    
   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, you’ll strip the list box 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 list box maintains and methods for manipulating them. In a real application, of course, these are a small fraction of the total methods of a list box, whose principal job is to display the strings and enable user choice.

The first thing to notice is the two private members:

private string[] myStrings;
private int myCtr = 0;

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

You initialize the array in the constructor with the statement:

myStrings = new String[256];

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

Tip

Because you cannot know how many strings will be added, you 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 you 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 or 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 >= myStrings.Length)
    {
       // handle bad index
    }
    return myStrings[index];
}

The set( ) method checks to make sure that the index you are setting already has a value in the list box. 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 which represents whatever is assigned using the index operator:

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

Thus, if you write:

myIndexedObject[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.

Indexers and Assignment

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

myIndexedObject[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:

myIndexedObject[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");

You 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, you modify the second value (at index 1):

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

Finally, you display each value in a loop:

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

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 which 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 list box, we might want to be able to index into the list box 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
   {
      // 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;
      }

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

   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 which 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 was done 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;

Tip

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

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.117.187.62