Using Indexers - Microsoft® Visual C#® 2012 Step by Step (2012)

Microsoft® Visual C#® 2012 Step by Step (2012)

Chapter 16. Using Indexers

After completing this chapter, you will be able to

§ Encapsulate logical arraylike access to an object by using indexers.

§ Control read access to indexers by declaring get accessors.

§ Control write access to indexers by declaring set accessors.

§ Create interfaces that declare indexers.

§ Implement indexers in structures and classes that inherit from interfaces.

Chapter 15, described how to implement and use properties as a means of providing controlled access to the fields in a class. Properties are useful for mirroring fields that contain a single value. However, indexers are invaluable if you want to provide access to items that contain multiple values by using a natural and familiar syntax.

What Is an Indexer?

You can think of an indexer as a smart array in much the same way that you can think of a property as a smart field. Where a property encapsulates a single value in a class, an indexer encapsulates a set of values. The syntax that you use for an indexer is exactly the same as the syntax that you use for an array.

The best way to understand indexers is to work through an example. First, you’ll consider a problem and examine a solution that doesn’t use indexers. Then you’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 container that holds an integer value. However, sometimes programmers use the int type for other purposes—some programs use an int as a set of binary flags and manipulate the individual bits within an int. If you are an old C hack like I am, what follows should have a very familiar feel!

NOTE

Some older programs used int types to try to save memory. Such programs typically date back to when the size of computer memory was measured in kilobytes rather than the gigabytes available these days and memory was at an absolute premium. A single int holds 32 bits, each of which can be 1 or 0. In some cases, programmers assigned 1 to indicate the value true and 0 to indicate false, and then employed an int as a set of Boolean values.

C# provides a set of operators that you can use to access and manipulate the individual bits in an int. These operators are as follows:

§ The NOT (~) operator. This 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)—all the 1s in the original value become 0s, and all the 0s become 1s.

NOTE

The examples shown here are purely illustrative and are accurate only to 8 bits. In C#, the int type is 32 bits, so if you try any these examples in a C# application, you will get a 32-bit result that may be different from those shown in this list. For example, in 32 bits, 204 is 00000000000000000000000011001100, so in C#, ~204 is 11111111111111111111111100110011 (which is actually theint representation of –205 in C#).

§ The left-shift (<<) operator. This 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 zeros are introduced from the right. There is a corresponding right-shift operator, >>.

§ The OR (|) operator. This 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 AND (&) operator. This 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 & 24 is 8 (204 is 11001100, 24 is 00011000, and 8 is00001000).

§ The XOR (^) operator. This operator performs a bitwise 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).

You can use these operators together to determine the values of the individual bits in an int. As an example, the following expression uses the left-shift (<<) and bitwise AND (&) operators to determine whether the sixth bit from the right of the byte variable named bits is set to 0 or to 1:

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

NOTE

The bitwise operators count the positions of bits from right to left, and the bits are numbered starting at 0. So bit 0 is the rightmost bit, and the bit at position 5 is the bit six places from the right.

Suppose the bits variable contains the decimal value 42. In binary, this is 00101010. The decimal value 1 is 00000001 in binary, and the expression 1 << 5 has the value 00100000; the sixth bit is 1. In binary, the expression bits & (1 << 5) is 00101010 & 00100000, and the value of this expression is binary 00100000, which is nonzero. If the variable bits contains the value 65, or 01000001 in binary, the value of the expression is 01000001 & 00100000, which yields the binary result 00000000, or zero.

This is a fairly complicated example, but it’s trivial in comparison to the following expression, which uses the compound assignment operator &= to set the bit at position 6 to 0:

bits &= ~(1 << 5)

Similarly, if you want to set the bit at position 6 to 1, you can use a bitwise OR (|) operator. The following complicated expression is based on the compound assignment operator |=:

bits |= (1 << 5)

The trouble with these examples is that although they work, they are fiendishly difficult to understand. They’re complicated, and the solution is a very low-level one: it fails to create an abstraction of the problem that it solves, and it is consequently very difficult to maintain code that performs these kinds of operations.

The Same Example Using Indexers

Let’s pull back from the preceding low-level solution for a moment for a reminder of what the problem is. You’d like to use an int not as an int but as an array of bits. Therefore, the best way to solve this problem is to use an int as if it were an array of bits! In other words, what you’d like to be able to write to access the bit 6 places from the right in the bits variable is an expression like this (remember that arrays start with index 0):

bits[5]

And to set the bit 4 places from the right to true, we’d like to be able to write this:

bits[3] = true

NOTE

To seasoned C developers, the Boolean value true is synonymous with the binary value 1, and the Boolean value false is synonymous with the binary value 0. Consequently, the expression bits[3] = true means “Set the bit 4 places from the right of the bits variable to 1.”

Unfortunately, you can’t use the square bracket notation on an int—it works only on an array or on a type that behaves like an array. 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. You can achieve this feat by defining an indexer. Let’s call this new type IntBits. IntBits will contain an int value (initialized in its constructor), but the idea is that you’ll use IntBits as an array of bool variables.

TIP

The IntBits type is small and lightweight, so it makes sense to create it as a structure rather than as a class.

struct IntBits

{

private int bits;

public IntBits(int initialBitValue)

{

bits = initialBitValue;

}

// indexer to be written here

}

To define the indexer, you use a notation that is a cross between a property and an array. You introduce the indexer with the this keyword, specify the type of the value returned by the indexer, and also specify the type of the value to use as the index into the indexer between square brackets. The indexer for the IntBits struct uses an integer as its index type and returns a Boolean value. It 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 points:

§ An indexer is not a method—there are no parentheses containing a parameter, but there are square brackets that specify an index. This index is used to specify which element is being accessed.

§ All indexers use the this keyword. A class or structure can define at most one indexer (although it can be overloaded and have several implementations), and it is always named this.

§ Indexers contain get and set accessors just like properties. In this example, the get and set accessors contain the complicated bitwise expressions previously discussed.

§ The index 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 you have declared the indexer, you can use a variable of type IntBits instead of an int and apply the square bracket notation, as shown in the next example:

int adapted = 126; // 126 has the binary representation 01111110

IntBits bits = new IntBits(adapted);

bool peek = bits[6]; // retrieve bool at index 6; should be true (1)

bits[0] = true; // set the bit at index 0 to true (1)

bits[3] = false; // set the bit at index 3 to false (0)

// the value in bits is now 01110111, or 119 in decimal

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

Understanding Indexer Accessors

When you read an indexer, the compiler automatically translates your arraylike code into a call to the get accessor of that indexer. Consider the following example:

bool peek = bits[6];

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

Similarly, if you write to an indexer, the compiler automatically translates your arraylike code into a call to the set accessor of that indexer, setting the index argument to the value enclosed in the square brackets. For example:

bits[3] = true;

This statement is converted to a call to the set accessor for bits where index is 3. As with ordinary properties, the data 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 both used. Look at the following statement, which uses the XOR operator (^) to invert the value of the bit at index 6 in the bits variable:

bits[6] ^= true;

This code is automatically translated into the following:

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

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

NOTE

You can declare an indexer that contains only a get accessor (a read-only indexer) or only a set accessor (a write-only accessor).

Comparing Indexers and Arrays

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

§ Indexers can use non-numeric subscripts, such as a string as shown in the following example. Arrays can use only integer subscripts:

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

§ Indexers can be overloaded (just like methods), whereas arrays cannot:

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

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

§ Indexers cannot 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 structure that exposes an array property named Data:

struct Wrapper

{

private 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 structure. Any changes you make to elements in myData are made to the data array; the expression myData[0]++ has exactly the same effect as data[0]++. If this is not the intention, you should use the Clone method in the getand set accessors of the Data property to return a copy of the data array, or make a copy of the value being set, as shown here. (Chapter 8, introduced the Clone method for copying arrays.) Notice that the Clone method returns an object, which you must cast to an integer array.

struct Wrapper

{

private int[] data;

...

public int[] Data

{

get { return this.data.Clone() as int[]; }

set { this.data = value.Clone() as int[]; }

}

}

However, this approach 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

{

private 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!

Indexers in Interfaces

You can declare indexers in an interface. To do this, specify the get keyword, the set keyword, or both, but replace the body of the get or set accessor with a semicolon. Any class or structure that implements the interface must implement the indexer accessors declared in the interface. For example:

interface IRawInt

{

bool this [ int index ] { get; set; }

}

struct RawInt : IRawInt

{

...

public bool this [ int index ]

{

get { ... }

set { ... }

}

...

}

If you implement the interface indexer in a class, you can declare the indexer implementations as virtual. This allows further derived classes to override the get and set accessors. For example:

class RawInt : IRawInt

{

...

public virtual bool this [ int index ]

{

get { ... }

set { ... }

}

...

}

You can also choose to implement an indexer by using the explicit interface implementation syntax covered in Chapter 12 An explicit implementation of an indexer is nonpublic and nonvirtual (and so cannot be overridden). For example:

struct RawInt : IRawInt

{

...

bool IRawInt.this [ int index ]

{

get { ... }

set { ... }

}

...

}

Using Indexers in a Windows Application

In the following exercise, you will examine a simple phone book application and complete its implementation. You will write two indexers in the PhoneBook class: one that accepts a Name parameter and returns a PhoneNumber, and another that accepts a PhoneNumber parameter and returns a Name. (The Name and PhoneNumber structures have already been written.) You will also need to call these indexers from the correct places in the program.

Familiarize yourself with the application

1. Start Microsoft Visual Studio 2012 if it is not already running.

2. Open the Indexers project, located in the \Microsoft Press\Visual CSharp Step By Step\Chapter 16\Windows X\Indexers folder in your Documents folder.

This graphical application enables a user to search for the telephone number for a contact and also find the name of a contact that matches a given telephone number.

3. On the DEBUG menu, click Start Debugging.

The project builds and runs. A form appears, displaying two empty text boxes labeled Name and Phone Number. The form also contains three buttons: one to add a name/phone number pair to a list of names and phone numbers held by the application, one to find a phone number when given a name, and one to find a name when given a phone number. These buttons currently do nothing.

If you are using Windows 7, the application looks like this:

image with no caption

If you are using Windows 8, the Add button is located in the app bar rather than on the main form. Remember that in Windows 8 you can display the app bar by right-clicking in the form displaying the application.

image with no caption

Your task is to complete the application so that the buttons work.

4. Return to Visual Studio 2012 and stop debugging.

5. Display the Name.cs file for the Indexers project in the Code and Text Editor window. Examine the Name structure. Its purpose is to act as a holder for names.

The name is provided as a string to the constructor. The name can be retrieved by using the read-only string property named Text. (The Equals and GetHashCode methods are used for comparing Names when searching through an array of Name values—you can ignore them for now.)

6. Display the PhoneNumber.cs file in the Code and Text Editor window, and examine the PhoneNumber structure. It is similar to the Name structure.

7. Display the PhoneBook.cs file in the Code and Text Editor window, and examine the PhoneBook class.

This class contains two private arrays: an array of Name values called names, and an array of PhoneNumber values called phoneNumbers. The PhoneBook class also contains an Add method that adds a phone number and name to the phone book. This method is called when the user clicks the Add button on the form. The enlargeIfFull method is called by Add to check whether the arrays are full when the user adds another entry. This method creates two new bigger arrays, copies the contents of the existing arrays to them, and then discards the old arrays.

The Add method is deliberately kept simple and does not check whether a name or phone number has already been added to the phone book.

The PhoneBook class does not currently provide any functionality enabling a user to find a name or telephone number; you will add two indexers to provide this facility in the next exercise.

Write the indexers

1. In the PhoneBook.cs file, delete the comment // TODO: write 1st indexer here and replace it with a public read-only indexer to the PhoneBook class, as shown in bold in the following code. The indexer should return a Name and take a PhoneNumber item as its index. Leave the body of the get accessor blank.

The indexer should look like this:

sealed class PhoneBook

{

...

public Name this[PhoneNumber number]

{

get

{

}

}

...

}

2. Implement the get accessor as shown in bold in the following code. The purpose of the accessor is to find the name that matches the specified phone number. To do this, you need to call the static IndexOf method of the Array class. The IndexOf method performs a search through an array, returning the index of the first item in the array that matches the specified value. The first argument to IndexOf is the array to search through (phoneNumbers). The second argument to IndexOf is the item you are searching for. IndexOf returns the integer index of the element if it finds it; otherwise, IndexOf returns –1. If the indexer finds the phone number, it should return it; otherwise, it should return an empty Name value. (Note that Name is a structure, so the default constructor sets its private name field to null.)

3. sealed class PhoneBook

4. {

5. ...

6. public Name this [PhoneNumber number]

7. {

8. get

9. {

10. int i = Array.IndexOf(this.phoneNumbers, number);

11. if (i != -1)

12. {

13. return this.names[i];

14. }

15. else

16. {

17. return new Name();

18. }

19. }

20. }

21. ...

}

22.Remove the comment // TODO: write 2nd indexer here and replace it with a second public read-only indexer to the PhoneBook class that returns a PhoneNumber and accepts a single Name parameter. Implement this indexer in the same way as the first one. (Again note thatPhoneNumber is a structure and therefore always has a default constructor.)

The second indexer should look like this:

sealed class PhoneBook

{

...

public PhoneNumber this [Name name]

{

get

{

int i = Array.IndexOf(this.names, name);

if (i != -1)

{

return this.phoneNumbers[i];

}

else

{

return new PhoneNumber();

}

}

}

...

}

Notice that these overloaded indexers can coexist because they return different types, which means that their signatures are different. If the Name and PhoneNumber structures were replaced by simple strings (which they wrap), the overloads would have the same signature and the class would not compile.

23.On the BUILD menu, click Build Solution. Correct any syntax errors, and then rebuild if necessary.

Call the indexers

1. Display the MainWindow.xaml.cs file in the Code and Text Editor window, and then locate the findByNameClick method.

This method is called when the Find by Name button is clicked. This method is currently empty. Replace the // TODO: comment with the code shown in bold in the following example. This code performs these tasks:

a. Read the value of the Text property from the name text box on the form. This is a string containing the contact name that the user has typed in.

b. If the string is not empty, search for the phone number corresponding to that name in the PhoneBook by using the indexer. (Notice that the MainWindow class contains a private PhoneBook field named phoneBook.) Construct a Name object from the string, and pass it as the parameter to the PhoneBook indexer.

c. If the Text property of the PhoneNumber structure returned by the indexer is not null or empty, write the value of this property to the phoneNumber text box on the form; otherwise, display the text “Not Found”.

The completed findByNameClick method should look like this:

private void findByNameClick(object sender, RoutedEventArgs e)

{

string text = name.Text;

if (!String.IsNullOrEmpty(text))

{

Name personsName = new Name(text);

PhoneNumber personsPhoneNumber = this.phoneBook[personsName];

phoneNumber.Text = String.IsNullOrEmpty(personsPhoneNumber.Text) ?

"Not Found" : personsPhoneNumber.Text;

}

}

Other than the statement that accesses the indexer, there are two further points of interest in this code:

d. The static String method IsNullOrEmpty is used to determine whether a string is empty or contains a null value. This is the preferred method for testing whether a string contains a value. It returns true if the string has a non-null value and false otherwise.

e. The ? : operator used by the statement that popula tes the Text property of the phoneNumber text box on the form acts like an inline if...else statement for an expression. It is a ternary operator that takes the following three operands: a Boolean expression, an expression to evaluate and return if the Boolean expression is true, and another expression to evaluate and return if the Boolean expression is false. In the preceding code, if the expression String.IsNullOrEmpty(personsPhoneNumber.Text) is true, no matching entry was found in the phone book and the text “Not Found” is displayed on the form; otherwise, the value held in the Text property of the personsPhoneNumber variable is displayed.

§ The general form of the ? : operator is as follows:

Result = <Boolean Expression> ? <Evaluate if true> : <Evaluate if false>

2. Locate the findByPhoneNumberClick method in the MainWindow.xaml.cs file. It is below the findByNameClick method.

The findByPhoneNumberClick method is called when the Find by Phone Number button is clicked. This method is currently empty apart from a // TODO: comment. You need to implement it as follows (the completed code is shown in bold in the following example):

a. Read the value of the Text property from the phoneNumber text box on the form. This is a string containing the phone number that the user has typed.

b. If the string is not empty, search for the name corresponding to that phone number in the PhoneBook by using the indexer.

c. Write the Text property of the Name structure returned by the indexer to the name text box on the form.

The completed method should look like this:

private void findByPhoneNumberClick(object sender, RoutedEventArgs e)

{

string text = phoneNumber.Text;

if (!String.IsNullOrEmpty(text))

{

PhoneNumber personsPhoneNumber = new PhoneNumber(text);

Name personsName = this.phoneBook[personsPhoneNumber];

name.Text = String.IsNullOrEmpty(personsName.Text) ?

"Not Found" : personsName.Text;

}

}

3. On the BUILD menu, click Build Solution. Correct any errors that occur.

Test the application

1. On the DEBUG menu, click Start Debugging.

2. Type your name and phone number in the text boxes, and then click Add.

When you click the Add button, the Add method stores the information in the phone book and clears the text boxes so they are ready to perform a search.

3. Repeat step 2 several times with some different names and phone numbers so that the phone book contains a selection of entries. Note that the application performs no checking of the names and telephone numbers that you enter, and you can input the same name and telephone number more than once. To avoid confusion, please make sure that you provide different names and telephone numbers.

4. Type a name that you used in step 3 into the Name text box, and then click Find by Name.

The phone number you added for this contact in step 3 is retrieved from the phone book and is displayed in the Phone Number text box.

5. Type a phone number for a different contact in the Phone Number text box, and then click Find by Phone Number.

The contact name is retrieved from the phone book and is displayed in the Name text box.

6. Type a name that you did not enter in the phone book into the Name text box, and then click Find by Name.

This time, the Phone Number text box displays the message “Not Found”.

7. Close the form, and return to Visual Studio 2012.

Summary

In this chapter, you saw how to use indexers to provide arraylike access to data in a class. You learned how to create indexers that can take an index and return the corresponding value by using logic defined by the get accessor, and you saw how to use the set accessor with an index to populate a value in an indexer.

§ If you want to continue to the next chapter

Keep Visual Studio 2012 running, and turn to Chapter 17.

§ If you want to exit Visual Studio 2012 now

On the FILE menu, click Exit. If you see a Save dialog box, click Yes and save the project.

Chapter 16 Quick Reference

To

Do this

Create an indexer for a class or structure

Declare the type of the indexer, followed by the keyword this and then the indexer arguments in square brackets. The body of the indexer can contain a get and/or set accessor. For example:

struct RawInt

{

...

public bool this [ int index ]

{

get { ... }

set { ... }

}

...

}

Define an indexer in an interface

Define an indexer with the get and/or set keywords. For example:

interface IRawInt

{

bool this [ int index ] { get; set; }

}

Implement an interface indexer in a class or structure

In the class or structure that implements the interface, define the indexer and implement the accessors. For example:

struct RawInt : IRawInt

{

...

public bool this [ int index ]

{

get { ... }

set { ... }

}

...

}

Implement an indexer defined by an interface by using explicit interface implementation in a class or structure

In the class or structure that implements the interface, specify the interface, but do not specify the indexer accessibility. For example:

struct RawInt : IRawInt

{

...

bool IRawInt.this [ int index ]

{

get { ... }

set { ... }

}

...

}