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.
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.
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
.
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]); }
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;
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.
18.225.234.28