Indexers: Using Objects Like Arrays

   


As we have seen earlier, an array contains many useful built-in methods and properties, such as Sort and Length. Sometimes, however, we need other abilities than those offered by the Array class. Perhaps we want an array that can find the sum and the average of its elements or display their values on a graphical user interface in a specific style. In those cases, we can write a class that essentially is an array (it would likely have an array or another collection as an instance variable) but with the extra functionality included.

Remember that to access a specific element of an array, we insert the element's index number in square brackets after the name of the array as follows:

 ...accounts[4]... 

So if an object essentially represents an array, as described here, shouldn't we be able to access the elements of this "array" object in the same manner (with the index and the square brackets) as a conventional array? The C# design team answered yes to this question, and included the indexer construct to support this ability.

Note

graphics/common.gif

Indexers offer an intuitive syntax to access the elements of a collection encapsulated by an object. The syntax consists of square brackets enclosing an index argument written after the name of the object, as shown in the following example:

 ...myArrayObject[4]... 

The indexer concept is similar to that of the property. A property pretends to be a field, but, in reality, executes get and set statement blocks an indexer pretends to be an array, but, in reality, also executes get and set statement blocks. Consequently, the syntax for implementing the two constructs is closely related.


Lines 7 17 of Listing 14.4 show a simple indexer positioned inside the BirthsList class. The indexer allows the Main method (lines 22 35) to access the births array (line 5) by treating the BirthsList object bLDenmark like an array (lines 27 30 and line 32).

Listing 14.4 BirthsList.cs
01: using System; 02: 03: class BirthsList 04: { 05:     private uint[] births = new uint[4]; 06: 07:     public uint this [uint index] 08:     { 09:         get 10:         { 11:             return births[index]; 12:         } 13:         set 14:         { 15:             births[index] = value; 16:         } 17:     } 18: } 19: 20: class BirthListTester 21: { 22:     public static void Main() 23:     { 24:         BirthsList bLDenmark = new BirthsList(); 25:         uint sum; 26: 27:         bLDenmark[0] = 10200; 28:         bLDenmark[1] = 20398; 29:         bLDenmark[2] = 40938; 30:         bLDenmark[3] = 6894; 31: 32:         sum = bLDenmark[0] + bLDenmark[1] + bLDenmark[2] + bLDenmark[3]; 33: 34:         Console.WriteLine("Sum the four regions: { 0} ", sum); 35:     } 36: } Sum the four regions: 78430 

The following analysis should be read in conjunction with Syntax Box 14.2.

The indexer of the BirthsList class includes a get statement block (lines 9 12) and a set statement block (lines 13 16). Line 27 resembles the syntax of assigning a value to an array element and triggers the set statement block to be executed. Just prior to this execution, two assignments are automatically performed by the runtime. First, the value of the index argument specified in line 27 (0, in this case) is automatically assigned to the parameter index specified in line 7. Second, the implicitly defined parameter value of the set statement block is assigned the value on the right side of the assignment statement in line 27 (10200, in this case). At the time line 15 is being executed, it can be written as follows:

 births[0] = 10200; 

which assigns 10200 to the first element of the array births.

bLDenmark[0] (line 32) is retrieving a value, from index 0 in this case. This triggers the get statement block of the indexer to be executed. The runtime automatically inserts the specified index argument 0 into the parameter index prior to the execution of line 11, which then can be written as follows:

 return births[0]; 

This returns the value of the first element of the births array back to bLDenmark[0].

Even though similar, indexers hold several important differences from their property siblings, as you will discover if you look closer at Syntax Box 14.2:

  • An indexer cannot be declared static. It must be an instance member, so only objects and not classes can be accessed with the square bracket notation.

  • An indexer is identified through the object in which it resides and through the combination of arguments provided to it in the square bracket pair following the object name. Consequently, an indexer has no name in itself. Because we treat the object like an array and the indexer remains anonymous, the keyword this is always used as the identifier in the declaration of the indexer.

  • The indexer declaration header must include a formal parameter list with at least one formal parameter.

Sytax Box 14.2 Indexer Declaration

 Indexer_declaration::= [<Access_modifier>] [override | new [ virtual | abstract ] ] <Type> graphics/ccc.gif this  [<Formal_parameter_list>] {       [<get_statement_block>]       [<set_statement_block>] } 

where

 <Access_modifier>                 ::= public                 ::= private                 ::= protected                 ::= internal                 ::= protected internal <get_statement_block> ::=           get           {               [<Statements>]               return <Expression>           } <set_statement_block> ::=           set           {               [<Statements (using the keyword value)>]           } 

Notes:

  • The optional keyword new of the property declaration header is semantically different than when you use it for creating new objects. Like the keywords override, virtual, and abstract, it is related to OO concepts we haven't discussed yet. I have included them here for completeness; they can safely be ignored for now.

  • The get and set statement blocks are often collectively referred to as accessors.

  • The formal parameter list can include parameters of any type. For example, it is possible to declare a formal parameter to be of type string. The corresponding index argument would then also be of type string. This is significantly different from the conventional array that only permits an index argument that either is or can be implicitly converted to one of the following types: uint, int, ulong, or long.

Implementing the Illusion of an Array

graphics/common.gif

Although an object with an indexer seems to represent an array (from the object user's perspective), it is up to you how this illusion is implemented inside the class. Instead of using an array to store the list of data (as in line 5 of Listing 14.4), you could, for example, use an ArrayList or another more suitable collection class. The most important is that the object behaves according to the client's expectations.


Note

graphics/common.gif

Even though an object is accessed as an array, it does not feature the methods and properties of a conventional array. For example, it would be invalid to access the length of bLDenmark in Listing 14.4 with the following call:

 ...bLDenmark.Length... 


Calling an Indexer from within the Object It Inhabits

We already know how to call a method from within the object where it resides. This is done by writing the name of the method together with the appropriate arguments, as follows:


graphics/14infig01a.gif

Sometimes, we might also want to use the functionality of an indexer from within the object it inhabits. Because the indexer is nameless and is identified through its object, writing a name equivalent to PrintMenu() will not work. Fortunately, we can use the this keyword's ability to reference the object in which it resides. By positioning the square brackets enclosing suitable arguments after the this keyword, as shown in the following example code, we can refer to an indexer of the same object.


graphics/14infig01.gif

Listing 14.5, presented in the next section, provides a complete program that utilizes the ability to call an indexer with the this keyword.

Indexer Overloading: Multiple Indexers in the Same Class

It is possible to include several indexers in the same class, but, because they all have the same name (this), each indexer must have a unique indexer signature. The meaning of indexer signature is nearly equivalent to that of the method signature discussed in Chapter 12, "Class Anatomy Part I: static Class Members and Method Adventures."

The amount, types, and sequence of the indexer's formal parameter list constitute the indexer's signature. It does not include any name, as does the method signature.

For example, the next indexer has int, int as its signature:

 public int this [int idxRow, int idxColumn] {       get        ...       set       ... } 

It is important to keep the following points in mind when using the signature concept.

  • The element type is not included in the indexer signature. Thus, the following two indexers represent the same signatures even though the first element type is double and the second element type is int:

     public double this [int idxRow, int idxColum] public int this [int idxRow, int idxColumn] 
  • The names of the formal parameters are not included in the method's signature, so the two displayed method headers have the same signature:

     public int this [int idxRow, int idxColumn] public int this [int a, int b] 

When several indexers exist in the same class, each with a unique signature, those indexers are called overloaded indexers.

The discussion about method overloading in Chapter 12 showed you how the arguments of a method call are used to determine which of several overloaded methods to execute. This process matches the types, number, and sequence of the arguments provided in a method call with a suitable method signature. The compiler uses exactly the same process (including the rules involving implicit conversion paths mentioned in Chapter 12) for determining which indexer to execute by matching the given index arguments with a suitable indexer signature. Figure 14.2 shows a simple example with two overloaded indexers residing inside the RainfallList class. Both indexer signatures consists of one parameter, but they are distinguished by their types: string and int, respectively. The rainInUSA[10] and rainInUSA["California"] calls to the indexer trigger the execution of two different indexers because they involve different index argument types.

Figure 14.2. Two overloaded indexers.
graphics/14fig02.gif

Listing 14.5 demonstrates the use of two overloaded indexers (lines 18 43 and lines 45 55) as well as how indexers can be called from within their own object by using the this keyword (lines 49 and 53) as mentioned earlier in this section. It also shows that properties do not necessarily have to access a particular instance variable but can return a calculated value instead.

Listing 14.5 AdvancedBirthsList.cs
 01: using System;  02:  03: class BirthsList  04: {  05:     private int[] births;  06:     private string[] birthsRegionNames;  07:  08:     public BirthsList(params string[] regionNames)  09:     {  10:         birthsRegionNames = regionNames;  11:         births = new int[regionNames.Length];  12:         for (int i = 0; i < regionNames.Length; i++)  13:         {  14:             births[i] = 0;  15:         }  16:     }  17:  18:     public int this [int index]  19:     {  20:         get  21:         {  22:             if (index >= 0 && index < births.Length)  23:             {  24:                 return births[index];  25:             }  26:             else  27:             {  28:                 Console.WriteLine("Incorrect index provided");  29:                 return -1;  30:             }  31:         }  32:         set  33:         {  34:             if (index >= 0 && index < births.Length)  35:             {  36:                 births[index] = value;  37:             }  38:             else  39:             {  40:                 Console.WriteLine("Incorrect index provided");  41:             }  42:         }  43:     }  44:  45:     public int this [string indexName]  46:     {  47:         get  48:         {  49:             return this [NameToIndex(indexName)];  50:         }  51:         set  52:         {  53:             this[NameToIndex(indexName)] = value;  54:         }  55:     }  56:  57:     private int NameToIndex(string indexName)  58:     {  59:         for (int i = 0; i < birthsRegionNames.Length; i++)  60:         {  61:             if (birthsRegionNames[i].ToUpper() == indexName.ToUpper())  62:                 return i;  63:         }  64:         Console.WriteLine("Could not find region name");  65:         return -1;  66:     }  67:  68:     public int TotalBirths  69:     {  70:         get  71:         {  72:             int sum = 0;  73:             foreach (int amount in births)  74:             {  75:                 sum += amount;  76:             }  77:             return sum;  78:         }  79:     }  80:   81:     public int Average  82:     {  83:         get  84:         {  85:             return TotalBirths / births.Length;  86:         }  87:     }  88: }  89:  90: class BirthListTester  91: {  92:     public static void Main()  93:     {  94:         BirthsList birthsListUSA = new BirthsList("California", "New York", "Texas");  95:  96:         birthsListUSA["California"] = 10200;  97:         birthsListUSA[1] = 20398;  98:         birthsListUSA[2] = 40938;  99: 100:         Console.WriteLine("Number of births in Texas: { 0} ",  graphics/ccc.gifbirthsListUSA["Texas"]); 101:         Console.WriteLine("Total births: { 0}    Average births: { 1} ", 102:             birthsListUSA.TotalBirths, birthsListUSA.Average); 103:     } 104: } Number of births in Texas: 40938 Total births: 71536   Average births: 23845 

The BirthsList class (lines 3 88) is a sophisticated version of the BirthsList class found in Listing 14.4. During its creation, tt allows us to specify the names of the regions for which we can store a births count. This ability is utilized in line 94. Each births count can be accessed either by specifying the name of the region (lines 96 and 100) or by specifying the index number (lines 97 and 98). The BirthsList class will even calculate the total births of all regions with its TotalBirths property (lines 68 79) and the average regional births count with the Average property (81 87). Both properties are called in line 102.

The BirthsList class relies on two arrays to provide its services. births (line 5) is used to hold the births count, and birthsRegionNames represents the region name of each array element. The latter is used by the NameToString method (lines 57 66) to convert a region name to a corresponding index number in the births array.

The familiar params keyword of the constructor header (line 8) lets the user include any number of region names when constructing a new BirthsList object, as demonstrated in line 94. The amount of region names provided determines the length of the births array in line 11.

Lines 12 16 are merely used to initialize all elements of the births array to zero.

The indexer contained in lines 18 43 has one formal parameter of type int, so it is called when the index argument is of type int as in lines 97 and 98. Both the set and the get statement blocks of this indexer contain code to verify that the given index argument is within the ranges of the births array. An invalid argument triggers the program to print an error message on the screen. A professional solution would probably generate an exception instead, but since we haven't discussed this mechanism yet, our current approach will suffice.

The indexer in lines 45 55 applies the keyword this in lines 49 and 53 to call its overloaded sibling of lines 18 43. According to our earlier discussion about the this keyword, a statement such as the following:

 this[1] = 10, 

will, if written inside the BirthsList class, call the set statement block in lines 32 42. As a result, it will assign 10 to the births array element of index 1. Similarly, the following line:

 return this[1]; 

will call the get statement block in lines 20 31, causing this[1] to represent the value of the second element in the births array. The NameToIndex(indexName) method call, found inside the square brackets of lines 49 and 53, returns the index or the births element corresponding to the indexName. Consequently, line 49 causes the value of the births element that corresponds to indexName to be returned to the original indexer call (line 100), and line 53 assigns the value represented by value to the births element corresponding to indexName.

Encapsulating the births collection in a class like BirthsList gives us free hands to include whatever functionality is needed when dealing with an object representing a list of births counts. Providing the total number of births in all regions and their average are just a couple of simple possibilities that, in this case, are calculated by using the two properties TotalBirths (lines 68 79) and Average (lines 81 87). Properties are often used to access instance variables, as you have seen earlier. However, a property can also return a purely calculated value that has no connection to any specific instance variable. TotalBirths and Average are both examples of those types of properties.

Avoid Overusing Indexers

There are two fundamental guidelines you should keep in mind before you implement indexers in your class:

  • Indexers should only be used in classes that essentially represent a collection.

  • The indexer should only be used to represent data that are part of the collection in a class and not any other instance variables.

Let's look at an example to illustrate the meaning of the last point. A programmer might decide to equip our now-familiar BirthsList class with two additional instance variables:

  • averageAge To hold the average age of the population that spans the area covered by the entire array of regions represented by a specific BirthsList object

  • marriedCouples To represent the number of married couples in this same area

The first part of the BirthsList class would then look like the following lines:

 class BirthsList {       private int[] births;       private string[] birthsRegionNames;       private int averageAge;       private int marriedCouples;       ... } 

A programmer writing this class might now incorrectly decide to use the indexer to access not only the births array (the true collection) but also the other instance variables averageAge and marriedCouples. He or she decides on a convention saying "averageAge is accessed when the client provides the index argument 0 and marriedCouples is accessed with the index argument 1."


graphics/14infig02.gif

This design is destined to fail. The user of the BirthsList class must remember what 0 and 1 represent. Was 0 the averageAge or was it marriedCouples? It's easy to mix up the two and, as a result, this is an error-prone path to follow. In general you should adhere to the following general guideline:

There should be as few literals in a program as possible. Evaluate every literal in your program and, if possible, replace it with something more descriptive.

We can remedy the disastrous approach and adhere to the guideline by using properties with proper names (AverageAge and MarriedCouples) for our instance variables instead. This would remove the doubt in anyone's mind as to how the two instance variables are accessed:

 birthsListUSA.AverageAge birthsListUSA.MarriedCouples 

   


C# Primer Plus
C Primer Plus (5th Edition)
ISBN: 0672326965
EAN: 2147483647
Year: 2000
Pages: 286
Authors: Stephen Prata

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