What Is an Indexer?


What Is an Indexer?

An indexer is a smart array in exactly the same way that a property is a smart field. The syntax that you use for an indexer is exactly the same as the syntax you use for an array. Let's work through an example. First, we'll examine a problem and see a weak solution that doesn't use indexers. Then we'll work through the same problem and look at a better solution that does use indexers. The problem concerns integers, or more precisely, the int type.

An Example That Doesn't Use Indexers

You normally use an int to hold an integer value. Internally an int stores its value as a sequence of 32 bits, where each bit can be either 0 or 1. Most of the time you don't care about this internal binary representation; you just use an int type as a bucket to hold an integer value. However, sometimes programmers use the int type for other purposes; some programs manipulate the individual bits within an int. (If you are an old C programmer you should feel at home with what follows!) In other words, occasionally a program might use an int because it holds 32 bits and not because it can represent an integer.

NOTE
Some older programs might use int types to try to save memory. A single int holds 32 bits, each of which can be 1 or 0. In some cases, programmers assigned 1 to indicate a value of true and 0 to indicate false, and then employed an int as a set of Boolean values.

For example, the following expression uses the << and & bit manipulation operators to find out whether the bit at index 6 of the int called bits is set to 0 or to 1:

(bits & (1 << 6)) != 0

If the bit at index 6 is 0, this expression evaluates to false; if the bit at index 6 is 1, this expression evaluates to true. This is a fairly complicated expression, but it's trivial in comparison to the following expression that sets the bit at index 6 to 0:

bits &= ~(1 << 6)

It's also trivial compared with this expression that sets the bit at index 6 to 1:

bits |= (1 << 6)

The trouble with these examples is that although they work, it's not clear why or how they work. They're complicated and the solution is a very low-level one. It fails to create an abstraction of the problem it solves.

The Bitwise and Shift Operators

You might have noticed some unfamiliar symbols in the expressions shown in these examples. In particular, ~, <<, |, and &. These are some of the bitwise and shift operators, and they are used to manipulate the individual bits held in the int and long data types.

The ~ operator is a unary operator that performs a bitwise complement. For example, if you take the 8-bit value 11001100 (204 decimal) and apply the ~ operator to it, you obtain the result 00110011 (51 decimal).

The << operator is a binary operator that performs a left-shift. The expression 204 << 2 returns the value 48 (in binary, 204 decimal is 11001100, and left-shifting it by two places yields 00110000, or 48 decimal). The far-left bits are discarded, and zeroes are introduced from the right. There is a corresponding right-shift operator >>.

The | operator is a binary operator that performs a bitwise OR operation, returning a value containing a 1 in each position in which either of the operands has a 1. For example, the expression 204 | 24 has the value 220 (204 is 11001100, 24 is 00011000, and 220 is 11011100).

The & operator performs a bitwise AND operation. AND is similar to the bitwise OR operator, except that it returns a value containing a 1 in each position where both of the operands have a 1. So 204 & 20 is 8 (204 is 11001100, 24 is 00011000, and 8 is 00001000).

The ^ operator performs a bitwise XOR (exclusive or) operation, returning a 1 in each bit where there is a 1 in one operand or the other, but not both. (Two 1s yield a 0—this is the “exclusive” part of the operator.) So 204 ^ 24 is 212 (11001100 ^ 00011000 is 11010100).

The Same Example Using Indexers

Let's pull back from the previous low-level solution for a moment and stop to remind ourselves what the problem is. We'd like to use an int not as an int but as an array of 32 bits. Therefore, the best way to solve this problem is to use an int as if it were an array of 32 bits! In other words, if bits is an int, what we'd like to be able to write to access the bit at index 6 is:

bits[6]

And, for example, set the bit at index 6 to true, we'd like to be able to write:

bits[6] = true

Unfortunately, you can't use the square bracket notation on an int. It only works on an array or on a type that behaves like an array; that is, on a type that declares an indexer. So the solution to the problem is to create a new type that acts like, feels like, and is used like an array of bool variables but is implemented by using an int. Let's call this new type IntBits. IntBits will contain an int value (initialized in its constructor), but the idea is that we'll use IntBits as an array of bool variables.

TIP
Because IntBits is small and lightweight, it makes sense to create it as a struct rather than as a class.

struct IntBits {     public IntBits(int initialBitValue)     {         bits = initialBitValue;     }     // indexer to be written here     private int bits; }

To define the indexer, you use a notation that is a cross between a property an an array. The indexer for the IntBits struct looks like this:

struct IntBits {     ...     public bool this [ int index ]     {         get          {             return (bits & (1 << index)) != 0;          }                  set          {             if (value)  // Turn the bit on if value is true, otherwise turn it off                 bits |=  (1 << index);             else                 bits &= ~(1 << index);         }     }     ... }

Notice the following:

  • An indexer is not a method; there are no parentheses, but there are square brackets.

  • An indexer always takes a single argument, supplied between the square brackets. This argument is used to specify which element is being accessed.

  • All indexers use the this keyword in place of the method name. A class or struct is allowed to define one indexer only, and it is always named this.

  • Indexers contain get and set accessors just like properties. The get and set accessors contain the complicated bitwise expressions previously discussed.

  • The argument specified in the indexer declaration is populated with the index value specified when the indexer is called. The get and set accessor methods can read this argument to determine which element should be accessed.

    NOTE
    You should perform a range check on the index value in the indexer to prevent any unexpected exceptions from occurring in your indexer code.

After the indexer has been declared, we can use a variable of type IntBits instead of an int and apply the square bracket notation as desired:

int adapted = 63; IntBits bits = new IntBits(adapted); bool peek = bits[6];  // retrieve bool at index 6 bits[0] = true;       // set the bit at index 0 to true bits[31] = false;     // set the bit at index 31 to false

This syntax is certainly much easier to understand. It directly and succinctly captures the essence of the problem.

NOTE
Indexers and properties are similar in that both use get and set accessors. An indexer is like a property with multiple values. However, although you're allowed to declare static properties, static indexers are illegal.

Understanding Indexer Accessors

When you read an indexer, the compiler automatically translates your array-like code into a call to the get accessor of that indexer. For example, consider the following example:

bool peek = bits[6];

This statement is converted into a call to the get accessor for bits, and the value of the index argument is set to 6.

Similarly, if you write to an indexer, the compiler automatically translates your array-like code into a call to the set accessor of that indexer, setting the index argument to the specified value. For example, consider the following statement:

bits[6] = true;

This statement is converted into a call to the set accessor for bits where the value of index is 6. As with ordinary properties, the value you are writing to the indexer (in this case, true) is made available inside the set accessor by using the value keyword. The type of value is the same as the type of indexer itself (in this case, bool).

It's also possible to use an indexer in a combined read/write context. In this case, the get and set accessors are used. For example, consider the following statement:

bits[6] ^= true;

This is automatically translated into:

bits[6] = bits[6] ^ true;

This code works because the indexer declares both a get and a set accessor.

NOTE
You're also allowed to declare an indexer that contains only a get accessor (a read-only indexer), or a set accessor (a write-only accessor).

Comparing Indexers and Arrays

When you use an indexer, the syntax is deliberately very array-like. However, there are some important differences between indexers and arrays:

  • Indexers can use non-numeric subscripts, whereas arrays can use only integer subscripts:

    public int this [ string name ] { ... } // okay

    TIP
    Many collection classes, such as Hashtable, that implement an associative lookup based on key/value pairs implement indexers as a convenient alternative to using the Add method to add a new value, and iterating through the Values property to locate a value in your code. For example, instead of this:

     Hashtable ages = new Hashtable(); ages.Add("John", 41);

    you can use this:

     Hashtable ages = new Hashtable(); ages["John"] = 41;

  • Indexers can be overloaded (just like methods), whereas arrays can't:

    public Name        this [ PhoneNumber number ] { ... } public PhoneNumber this [ Name name ] { ... }

  • Indexers can't be used as ref or out parameters, whereas array elements can:

    IntBits bits;        // bits contains an indexer Method(ref bits[1]); // compile-time error

Properties, Arrays, and Indexers

It is possible for a property to return an array, but remember that arrays are reference types, so exposing an array as a property makes it possible to accidentally overwrite a lot of data. Look at the following struct that exposes an array property called Data:

struct Wrapper {     int[] data;     ...     public int[] Data     {         get { return this.data; }         set { this.data = value; }     } }

Now consider the following code that uses this property:

Wrapper wrap = new Wrapper(); ... int[] myData = wrap.Data; myData[0]++; myData[1]++;

This looks pretty innocuous. However, because arrays are reference types, the variable myData refers to the same object as the private data variable in the Wrapper struct. Any changes you make to elements in myData are made to the data array; the statement myData[0]++ has exactly the same effect as data[0]++. If this is not the intention, it is possible to use the Clone method in the get accessor of the Data property to return a copy of the data array, but this can become very messy and expensive in terms of memory use. Indexers provide a natural solution to this problem—don't expose the entire array as a property, just make its individual elements available through an indexer:

struct Wrapper {     int[] data;     ...     public int this [int i]     {         get { return this.data[i]; }         set { this.data[i] = value; }     } }

The following code uses the indexer in a similar manner to the property shown earlier:

Wrapper wrap = new Wrapper(); ... int[] myData = new int[2]; myData[0] = wrap[0]; myData[1] = wrap[1]; myData[0]++; myData[1]++;

This time, incrementing the values in the MyData array has no effect on the original array in the Wrapper object. If you really want to modify the data in the Wrapper object, you must write statements such as this:

wrap[0]++;

This is much clearer, and safer!




Microsoft Visual C# 2005 Step by Step
Microsoft® Visual C#® 2005 Step by Step (Step By Step (Microsoft))
ISBN: B002CKYPPM
EAN: N/A
Year: 2005
Pages: 183
Authors: John Sharp

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