Section 9.3. Indexers


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.[2] For example, such a property would permit statements such as the following:

[2] The actual ListBox control provided by both Windows Forms and ASP.NET has a collection called Items, and it is the Items collection that implements the indexer.

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 accessors 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 accessors, 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 can't be overloaded in C#, which provides the indexer in its place.


Example 9-9. Using a simple indexer
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace SimpleIndexer {    // 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, we 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 can't 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 can't assign to an index that doesn't 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 code is kept simple and thus is not robust. There are any number of other checks you'll want to make on the value passed in (e.g., checking that you were not passed a negative index and that it doesn't exceed the size of the underlying strings[] array).


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# doesn't 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
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace OverloadedIndexer {    // 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 } 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 doesn't 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.



Programming C#(c) Building. NET Applications with C#
Programming C#: Building .NET Applications with C#
ISBN: 0596006993
EAN: 2147483647
Year: 2003
Pages: 180
Authors: Jesse Liberty

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