Indexers

 
Chapter 3 - Object-Oriented C#
bySimon Robinsonet al.
Wrox Press 2002
  

Indexers share with properties the fact that they are not really an essential part of object-oriented programming. Rather, they represent a syntactical convenience that allows certain classes to be used in a more intuitive manner. In the case of indexers, the convenience they allow is for you to access an object as if it was an array.

C++ developers should note that indexers in C# serve the same purpose as overriding the [] operator in C++. The concept of an indexer may be new to Java and VB developers, however.

Adding an Indexer to Vector

We're going to continue using our Vector struct as an example to demonstrate the use of indexers. As with operator overloads, indexers work in the same way for structs and classes, so the fact that we happen to be using a struct for our example is not significant.

Up to now we've referred to the components of our Vector struct with their names x , y , and z . The trouble is that mathematicians often prefer to treat vectors as if they are arrays, with x being the first element, y the second, and z the third. In other words, in order to set the x -component they will tend to write:

   MyVector[0] = 3.6;   

If we can treat Vector s as arrays then we should also be able to do things like iterate through the components:

   for(int i = 0; i < 3; i++)     {     vect2[i] = i;     }   

With our current definition of Vector , these code snippets will produce a compile-time error, since the compiler won't understand what we mean by the first-element of a Vector . Indexers are a way of solving that. If you define an indexer for a class, you are telling the compiler what to do if it encounters code in which a class instance is being treated as if it were an array.

Indexers are defined in pretty much the same way as properties, with get and set accessors. The main difference is that the name of the indexer is the keyword this . To define an indexer for the Vector struct, we modify the strut definition as follows :

 struct Vector    {       public double x, y, z;   public double this [int i]     {     get     {     switch (i)     {     case 0:     return x;     case 1:     return y;     case 2:     return z;     default:     throw new IndexOutOfRangeException(     "Attempt to retrieve Vector element " + i) ;     }     }     set     {     switch (i)     {     case 0:     x = value;     break;     case 1:     y = value;     break;     case 2:     z = value;     break;     default:     throw new IndexOutOfRangeException(     "Attempt to set Vector element " + i);     }     }     }   // etc. 

There's a fair bit of new stuff in this code. To start with, let's look at the line that declares the indexer:

 public double this [int i] 

This line basically says that we want to be able to treat each Vector instance as a one-dimensional array with an int as the index (or, equivalently in this case, the parameter), and that when we do so the return type is a double . Indexers actually give us quite a lot of freedom - we can use any data type as the index, though the types you'll most often want to use are the integer types and string . Similarly, you can use whatever data type you think most appropriate for the return type.

Within the body of our indexer, we have the same get and set accessors that you see for properties. If we wanted to make our indexer read-only or write-only, we could do so by leaving out the appropriate accessor. The syntax for the accessors follows precisely that which is used when you define properties, except that we now have accessors to whatever variables we defined as the parameters to the indexer (recall that property get and set accessors never take any explicit parameters). The get accessor must return the type we have declared the indexer as returning, while the set accessor must not return anything, and has access to an additional implicit parameter, value , whose data type is the type we declared the indexer as ( double in this case), and which is initialized as the value on the right-hand side of the assignment operator we use the indexer expression in.

Beyond that, the code should be self-explanatory to the extent that we simply use the parameter passed in to determine which of the components of the Vector should be accessed, and either return or set the appropriate field. Notice, however, that the switch statements also each have a default case to handle the situation in which the indexer is called with an inappropriate value for the parameter. The action taken in this case is to throw an exception:

 throw new IndexOutOfRangeException(                                "Attempt to retrieve Vector element " + i); 

We haven't yet encountered exceptions - those will be covered in the next chapter. They are the way that you deal with unexpected error conditions in C#, and up to now in this chapter I've carefully avoided any error checking in any of the examples, precisely because we haven't yet covered exceptions. For an indexer, however, you can't really get away without checking that the index passed in is within the appropriate bounds - for our Vector class, 0 to 2. So for the time being I'll just ask you to accept that the line above is the way you'd handle an index out of bounds. What it actually should do is cause execution to jump to a special area of code that you'll ideally have written and marked as responsible for handling this particular error situation. Since we haven't written any such code yet, this statement will instead cause program execution to terminate.

Now we've added our indexer, let's try it out:

 static void Main()       {   Vector vect1 = new Vector(1.0, -5.0, 4.6);     Vector vect2 = new Vector();     Console.WriteLine("vect1 = " + vect1);     Console.WriteLine("vect1[1] = " + vect1[1]);     for(int i = 0; i < 3; i++)     {     vect2[i] = i;     }     Console.WriteLine("vect2 = " + vect2);   

Note that in this example ( VectorWithIndexer ) we are demonstrating using a for loop to index the components of the Vector .

Although we are able to use for , do, and while loops with indexers, we cannot write a loop that uses foreach . The foreach statement works in a different way, treating the item as a collection rather than an array. It's possible to set up a class or struct so it acts as a collection, but that involves implementing certain interfaces rather than indexers. We'll show how to do this in Chapter 5.

This is the result:

  VectorWithIndexer  vect1 = ( 1 , -5 , 4.6 ) vect1[1] = -5 vect2 = ( 0 , 1 , 2 ) 

It is also possible to verify that our exception handling code does trap an out-of-bound index. If we try to access the Vector like this:

   double tryThis = vect1[6];   

Then our console application terminates and this dialog is displayed:

  VectorWithIndexer  vect1 = ( 1 , -5 , 4.6 ) vect1[1] = -5 Unhandled Exception: System.IndexOutOfRangeException: Attempt to retrieve Vector element 6    at Wrox.ProCSharp.OOCSharp.Vector.get_Item(Int32 i)    at Wrox.ProCSharp.OOCSharp.Vector.Main() 

In the next chapter, we'll see how to handle exceptions so that you can determine what action your program takes, rather than simply terminating.

Other Indexer Examples

Indexers are extremely flexible. They are not, for example confined to one-dimensional arrays. We can treat classes and structs as multidimensional arrays as well, just by adding more than one parameter inside the square brackets. We can also overload indexers - a struct or class can have as many indexers as you want, provided they have different numbers or types of parameters.

One example to illustrate this would be that if we'd wanted to write that Matrix class, we'd probably want to be able to treat it as a 2D array of double s. Not only that, but mathematicians would regard any individual row of the matrix as a vector. We won't actually work through the example, but in principle we could achieve this by defining two indexers like this:

   // for struct Matrix     public double this [uint i, uint j]     public Vector this [uint i]   

Another common use for indexers is to be able to access a part of a class or struct in a way that is described by a string. This means that you can have an array or some data structure that has named elements (although in this case, you might prefer to use a dictionary, as described in Chapter 5):

   // for a class, ListOfCustomers     public Customer this[string Name]     // in client code     Customer nextCustomer = CustomerList["Simon Robinson"]   

A useful variation of this is to access elements using an enumerated value.

Indeed, indexers are most commonly used for classes that represent some data structure, such as an array, a list, or a map, and are defined for the .NET base classes that represent these structures, which we'll examine in Chapter 5.

  


Professional C#. 2nd Edition
Performance Consulting: A Practical Guide for HR and Learning Professionals
ISBN: 1576754359
EAN: 2147483647
Year: 2002
Pages: 244

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