Chapter 10: Indexers and Properties


This chapter examines two special types of class members that have a close relationship to each other: indexers and properties. Each of these expands the power of a class by enhancing its integration into C#’s type system and improving its resiliency. Indexers provide the mechanism by which an object can be indexed like an array. Properties offer a streamlined way to manage access to a class’ instance data. They relate to each other because both rely upon another feature of C#: the accessor.

Indexers

As you know, array indexing is performed using the [ ] operator. It is possible to overload the [ ] operator for classes that you create, but you don’t use an operator method. Instead, you create an indexer. An indexer allows an object to be indexed like an array. The main use of indexers is to support the creation of specialized arrays that are subject to one or more constraints. However, you can use an indexer for any purpose for which an array-like syntax is beneficial. Indexers can have one or more dimensions. We will begin with one-dimensional indexers.

Creating One-Dimensional Indexers

A one-dimensional indexer has this general form:

 element-type this[int index] {   // The get accessor.   get {     // return the value specified by index   }   // The set accessor.   set {     // set the value specified by index   } }

Here, element-type is the base type of the indexer. Thus, each element accessed by the indexer will be of type element-type. The element type corresponds to the base type of an array. The parameter index receives the index of the element being accessed. Technically, this parameter does not have to be of type int, but since indexers are typically used to provide array indexing, an integer type is customary.

Inside the body of the indexer are defined two accessors that are called get and set. An accessor is similar to a method except that it does not have a return type or parameter declarations. The accessors are automatically called when the indexer is used, and both accessors receive index as a parameter. If the indexer is on the left side of an assignment statement, then the set accessor is called, and the element specified by index must be set. Otherwise, the get accessor is called, and the value associated with index must be returned. The set accessor also receives an implicit parameter called value, which contains the value being assigned to the specified index.

One of the benefits of an indexer is that you can control precisely how an array is accessed, heading off improper accesses. Here is an example. In the following program the FailSoftArray class implements an array that traps boundary errors, thus preventing runtime exceptions if the array is indexed out-of-bounds. This is accomplished by encapsulating the array as a private member of a class, allowing access to the array only through the indexer. With this approach, any attempt to access the array beyond its boundaries can be prevented, with such an attempt failing gracefully (resulting in a “soft landing” rather than a “crash”). Since FailSoftArray uses an indexer, the array can be accessed using the normal array notation:

 // Use an indexer to create a fail-soft array. using System; class FailSoftArray {   int[] a;    // reference to underlying array   public int Length; // Length is public   public bool errflag; // indicates outcome of last operation   // Construct array given its size.   public FailSoftArray(int size) {     a = new int[size];     Length = size;   }   // This is the indexer for FailSoftArray.   public int this[int index] {     // This is the get accessor.     get {       if(ok(index)) {         errflag = false;         return a[index];       } else {         errflag = true;         return 0;       }     }     // This is the set accessor     set {       if(ok(index)) {         a[index] = value;         errflag = false;       }       else errflag = true;     }   }   // Return true if index is within bounds.   private bool ok(int index) {    if(index >= 0 & index < Length) return true;    return false;   } } // Demonstrate the fail-soft array. class FSDemo {   public static void Main() {     FailSoftArray fs = new FailSoftArray(5);     int x;     // show quiet failures     Console.WriteLine("Fail quietly.");     for(int i=0; i < (fs.Length * 2); i++)       fs[i] = i*10;     for(int i=0; i < (fs.Length * 2); i++) {       x = fs[i];       if(x != -1) Console.Write(x + " ");     }     Console.WriteLine();     // now, generate failures     Console.WriteLine("\nFail with error reports.");     for(int i=0; i < (fs.Length * 2); i++) {       fs[i] = i*10;       if(fs.errflag)         Console.WriteLine("fs[" + i + "] out-of-bounds");     }     for(int i=0; i < (fs.Length * 2); i++) {       x = fs[i];       if(!fs.errflag) Console.Write(x + " ");       else         Console.WriteLine("fs[" + i + "] out-of-bounds");     }   } }

The output from the program is shown here:

 Fail quietly. 0 10 20 30 40 0 0 0 0 0 Fail with error reports. fs[5] out-of-bounds fs[6] out-of-bounds fs[7] out-of-bounds fs[8] out-of-bounds fs[9] out-of-bounds 0 10 20 30 40 fs[5] out-of-bounds fs[6] out-of-bounds fs[7] out-of-bounds fs[8] out-of-bounds fs[9] out-of-bounds

The indexer prevents the array boundaries from being overrun. Let’s look closely at each part of the indexer. It begins with this line:

 public int this[int index] {

This declares an indexer that operates on int elements. The index is passed in index. The indexer is public, allowing it to be used by code outside of its class.

The get accessor is shown here:

 get {   if(ok(index)) {     errflag = false;     return a[index];   } else {     errflag = true;     return 0;   } }

The get accessor prevents array boundary errors by first confirming that the index is not out-of-bounds. This range check is performed by the ok( ) method, which returns true if the index is valid, and false otherwise. If the specified index is within bounds, the element corresponding to the index is returned. If it is out of bounds, no operation takes place and no overrun occurs. In this version of FailSoftArray, a variable called errflag contains the outcome of each operation. This field can be examined after each operation to assess the success or failure of the operation.

The set accessor is shown here. It too prevents a boundary error.

 set {   if(ok(index)) {     a[index] = value;     errflag = false;   }   else errflag = true; }

Here, if index is within bounds, the value passed in value is assigned to the corresponding element. Otherwise, errflag is set to true. Recall that in an accessor method, value is an implicit parameter that contains the value being assigned. You do not need to (nor can you) declare it.

Indexers do not have to support both get and set. You can create a read-only indexer by implementing only the get accessor. You can create a write-only indexer by implementing only set.

Indexers Can Be Overloaded

An indexer can be overloaded. The version executed will be the one that has the closest type-match between its parameter and the argument used as an index. Here is an example that overloads the FailSoftArray indexer for indexes of type double. The double indexer rounds its index to the nearest integer value.

 // Overload the FailSoftArray indexer. using System; class FailSoftArray {   int[] a;    // reference to underlying array   public int Length; // Length is public   public bool errflag; // indicates outcome of last operation   // Construct array given its size.   public FailSoftArray(int size) {     a = new int[size];     Length = size;   }   // This is the int indexer for FailSoftArray.   public int this[int index] {     // This is the get accessor.     get {       if(ok(index)) {         errflag = false;         return a[index];       } else {         errflag = true;         return 0;       }     }     // This is the set accessor     set {       if(ok(index)) {         a[index] = value;         errflag = false;       }       else errflag = true;     }   }   /* This is another indexer for FailSoftArray.      This index takes a double argument.  It then      rounds that argument to the nearest integer      index. */   public int this[double idx] {     // This is the get accessor.     get {       int index;       // round to nearest int       if( (idx - (int) idx) < 0.5) index = (int) idx;       else index = (int) idx + 1;       if(ok(index)) {         errflag = false;         return a[index];       } else {         errflag = true;         return 0;       }     }     // This is the set accessor     set {       int index;       // round to nearest int       if( (idx - (int) idx) < 0.5) index = (int) idx;       else index = (int) idx + 1;       if(ok(index)) {         a[index] = value;         errflag = false;       }       else errflag = true;     }   }   // Return true if index is within bounds.   private bool ok(int index) {    if(index >= 0 & index < Length) return true;    return false;   } } // Demonstrate the fail-soft array. class FSDemo {   public static void Main() {     FailSoftArray fs = new FailSoftArray(5);     // put some values in fs     for(int i=0; i < fs.Length; i++)       fs[i] = i;     // now index with ints and doubles     Console.WriteLine("fs[1]: " + fs[1]);     Console.WriteLine("fs[2]: " + fs[2]);     Console.WriteLine("fs[1.1]: " + fs[1.1]);     Console.WriteLine("fs[1.6]: " + fs[1.6]);   } }

This program produces the following output:

 fs[1]: 1 fs[2]: 2 fs[1.1]: 1 fs[1.6]: 2

As the output shows, the double indexes are rounded to their nearest integer value. Specifically, 1.1 is rounded to 1, and 1.6 is rounded to 2.

Although overloading an indexer as shown in this program is valid, it is not common. Most often, an indexer is overloaded to enable an object of a class to be used as an index, with the index computed in some special way.

Indexers Do Not Require an Underlying Array

It is important to understand that there is no requirement that an indexer actually operate on an array. It simply must provide functionality that appears “array-like” to the user of the indexer. For example, the following program has an indexer that acts like a read-only array that contains the powers of 2 from 0 to 15. Notice, however, that no actual array exists. Instead, the indexer simply computes the proper value for a given index.

 // Indexers don't have to operate on actual arrays. using System; class PwrOfTwo {   /* Access a logical array that contains      the powers of 2 from 0 to 15. */   public int this[int index] {     // Compute and return power of 2.     get {       if((index >= 0) && (index < 16)) return pwr(index);       else return -1;     }     // there is no set accessor   }   int pwr(int p) {     int result = 1;     for(int i=0; i < p; i++)       result *= 2;     return result;   } } class UsePwrOfTwo {   public static void Main() {     PwrOfTwo pwr = new PwrOfTwo();     Console.Write("First 8 powers of 2: ");     for(int i=0; i < 8; i++)       Console.Write(pwr[i] + " ");     Console.WriteLine();     Console.Write("Here are some errors: ");     Console.Write(pwr[-1] + " " + pwr[17]);     Console.WriteLine();   } }

The output from the program is shown here:

 First 8 powers of 2: 1 2 4 8 16 32 64 128 Here are some errors: -1 -1

Notice that the indexer for PwrOfTwo includes a get accessor, but no set accessor. As explained, this means that the indexer is read-only. Thus, a PwrOfTwo object can be used on the right side of an assignment statement, but not on the left. For example, attempting to add this statement to the preceding program won’t work:

 pwr[0] = 11; // won't compile

This statement will cause a compilation error because there is no set accessor defined for the indexer.

There are two important restrictions to using indexers. First, because an indexer does not define a storage location, a value produced by an indexer cannot be passed as a ref or out parameter to a method. Second, an indexer must be an instance member of its class; it cannot be declared static.

Multidimensional Indexers

You can create indexers for multidimensional arrays, too. For example, here is a two-dimensional fail-soft array. Pay close attention to the way that the indexer is declared.

 // A two-dimensional fail-soft array. using System; class FailSoftArray2D {   int[,] a; // reference to underlying 2D array   int rows, cols; // dimensions   public int Length; // Length is public   public bool errflag; // indicates outcome of last operation   // Construct array given its dimensions.   public FailSoftArray2D(int r, int c) {     rows = r;     cols = c;     a = new int[rows, cols];     Length = rows * cols;   }   // This is the indexer for FailSoftArray2D.   public int this[int index1, int index2] {     // This is the get accessor.     get {       if(ok(index1, index2)) {         errflag = false;         return a[index1, index2];       } else {         errflag = true;         return 0;       }     }     // This is the set accessor.     set {       if(ok(index1, index2)) {         a[index1, index2] = value;         errflag = false;       }       else errflag = true;     }   }   // Return true if indexes are within bounds.   private bool ok(int index1, int index2) {    if(index1 >= 0 & index1 < rows &       index2 >= 0 & index2 < cols)          return true;    return false;   } } // Demonstrate a 2D indexer. class TwoDIndexerDemo {   public static void Main() {     FailSoftArray2D fs = new FailSoftArray2D(3, 5);     int x;     // show quiet failures     Console.WriteLine("Fail quietly.");     for(int i=0; i < 6; i++)       fs[i, i] = i*10;     for(int i=0; i < 6; i++) {       x = fs[i,i];       if(x != -1) Console.Write(x + " ");     }     Console.WriteLine();     // now, generate failures     Console.WriteLine("\nFail with error reports.");     for(int i=0; i < 6; i++) {       fs[i,i] = i*10;       if(fs.errflag)         Console.WriteLine("fs[" + i + ", " + i + "] out-of-bounds");     }     for(int i=0; i < 6; i++) {       x = fs[i,i];       if(!fs.errflag) Console.Write(x + " ");       else         Console.WriteLine("fs[" + i + ", " + i + "] out-of-bounds");     }   } }

The output from this program is shown here:

 Fail quietly. 0 10 20 0 0 0 Fail with error reports. fs[3, 3] out-of-bounds fs[4, 4] out-of-bounds fs[5, 5] out-of-bounds 0 10 20 fs[3, 3] out-of-bounds fs[4, 4] out-of-bounds fs[5, 5] out-of-bounds




C# 2.0(c) The Complete Reference
C# 2.0: The Complete Reference (Complete Reference Series)
ISBN: 0072262095
EAN: 2147483647
Year: 2006
Pages: 300

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