This chapter examines two special types of class members that have a close relationship to each other: indexers and properties. Each 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.
As you know, array indexing is performed using the [ ] operator. It is possible to define 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.
A simple 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 element type of the indexer. Thus, each element accessed by the indexer will be of type element-type. This type corresponds to the element 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, using an integer type is quite common.
Inside the body of the indexer two accessors are defined that are called get and set. An accessor is similar to a method except that it does not declare a return type or parameters. 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 method 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 access. 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 {
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, display 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. (In Chapter 13, you will see a better way to handle errors by using C#’s exception subsystem, but for now, using an error flag is an acceptable approach.)
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.
It is not necessary for an indexer 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.
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 {
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.
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 {
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 no set accessor is 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.
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 {
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, display 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
Another type of class member is the property. As a general rule, a property combines a field with the methods that access it. As some examples earlier in this book have shown, you will often want to create a field that is available to users of an object, but you want to maintain control over the operations allowed on that field. For instance, you might want to limit the range of values that can be assigned to that field. While it is possible to accomplish this goal through the use of a private variable along with methods to access its value, a property offers a better, more streamlined approach.
Properties are similar to indexers. A property consists of a name along with get and set accessors. The accessors are used to get and set the value of a variable. The key benefit of a property is that its name can be used in expressions and assignments like a normal variable, but in actuality the get and set accessors are automatically invoked. This is similar to the way that an indexer’s get and set accessors are automatically used.
The general form of a property is shown here:
type name {
get {
// get accessor code
}
// set accessor code
}
}
Here, type specifies the type of the property, such as int, and name is the name of the property. Once the property has been defined, any use of name results in a call to its appropriate accessor. The set accessor automatically receives a parameter called value that contains the value being assigned to the property.
It is important to understand that properties do not define storage locations. Instead, a property typically manages access to a field. It does not, itself, provide that field. The field must be specified independently of the property. (The exception is the auto-implemented property, which is described shortly.)
Here is a simple example that defines a property called MyProp, which is used to access the field prop. In this case, the property allows only positive values to be assigned.
// A simple property example.
using System;
class SimpProp {
int prop; // field being managed by MyProp
public SimpProp() { prop = 0; }
/* This is the property that supports access to
the private instance variable prop. It
allows only positive values. */
public int MyProp {
get {
return prop;
}
set {
if(value >= 0) prop = value;
}
}
}
// Demonstrate a property.
class PropertyDemo {
static void Main() {
SimpProp ob = new SimpProp();
Console.WriteLine("Original value of ob.MyProp: " + ob.MyProp);
ob.MyProp = 100; // assign value
Console.WriteLine("Value of ob.MyProp: " + ob.MyProp);
// Can't assign negative value to prop.
Console.WriteLine("Attempting to assign -10 to ob.MyProp");
ob.MyProp = -10;
Console.WriteLine("Value of ob.MyProp: " + ob.MyProp);
}
}
Output from this program is shown here:
Original value of ob.MyProp: 0
Value of ob.MyProp: 100
Attempting to assign -10 to ob.MyProp
Value of ob.MyProp: 100
Let’s examine this program carefully. The program defines one private field, called prop, and a property called MyProp that manages access to prop. As explained, a property by itself does not define a storage location. Instead, most properties simply manage access to a field. Furthermore, because prop is private, it can be accessed only through MyProp.
The property MyProp is specified as public so it can be accessed by code outside of its class. This makes sense because it provides access to prop, which is private. The get accessor simply returns the value of prop. The set accessor sets the value of prop if and only if that value is positive. Thus, the MyProp property controls what values prop can have. This is the essence of why properties are important.
The type of property defined by MyProp is called a read-write property because it allows its underlying field to be read and written. It is possible, however, to create read-only and write-only properties. To create a read-only property, define only a get accessor. To define a write-only property, define only a set accessor.
You can use a property to further improve the fail-soft array class. As you know, all arrays have a Length property associated with them. Up to now, the FailSoftArray class simply used a public integer field called Length for this purpose. This is not good practice, though, because it allows Length to be set to some value other than the length of the failsoft array. (For example, a malicious programmer could intentionally corrupt its value.) We can remedy this situation by transforming Length into a read-only property, as shown in the following version of FailSoftArray:
// Add Length property to FailSoftArray.
using System;
class FailSoftArray {
int[] a; // reference to underlying array
int len; // length of array -- underlies Length property
public bool ErrFlag; // indicates outcome of last operation
// Construct array given its size.
public FailSoftArray(int size) {
a = new int[size];
len = size;
}
// Read-only Length property.
public int Length {
get {
return len;
}
}
// 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 improved fail-soft array.
class ImprovedFSDemo {
static void Main() {
FailSoftArray fs = new FailSoftArray(5);
int x;
// Can read Length.
for(int i=0; i < fs.Length; i++)
fs[i] = i*10;
for(int i=0; i < fs.Length; i++) {
x = fs[i];
if(x != -1) Console.Write(x + " ");
}
Console.WriteLine();
// fs.Length = 10; // Error, illegal!
}
}
Length is now a property that uses the private variable len for its storage. Length defines only a get accessor, which means that it is read-only. Thus, Length can be read, but not changed. To prove this to yourself, try removing the comment symbol preceding this line in the program:
// fs.Length = 10; // Error, illegal!
When you try to compile, you will receive an error message stating that Length is read-only.
Although the addition of the Length property improves FailSoftArray, it is not the only improvement that properties can make. The ErrFlag member is also a prime candidate for conversion into a property since access to it should also be limited to read-only. Here is the final improvement of FailSafeArray. It creates a property called Error that uses the original ErrFlag variable as its storage, and ErrFlag is made private to FailSoftArray.
// Convert ErrFlag into a property.
using System;
class FailSoftArray {
int[] a; // reference to underlying array
int len; // length of array
bool ErrFlag; // now private
// Construct array given its size.
public FailSoftArray(int size) {
a = new int[size];
len = size;
}
// Read-only Length property.
public int Length {
get {
return len;
}
}
// Read-only Error property.
public bool Error {
get {
return ErrFlag;
}
}
// 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 improved fail-soft array.
class FinalFSDemo {
static void Main() {
FailSoftArray fs = new FailSoftArray(5);
// Use Error property.
for(int i=0; i < fs.Length + 1; i++) {
fs[i] = i*10;
if(fs.Error)
Console.WriteLine("Error with index " + i);
}
}
}
The creation of the Error property has caused two changes to be made to FailSoftArray. First, ErrFlag has been made private because it is now used as the underlying storage for the Error property. Thus, it won’t be available directly. Second, the read-only Error property has been added. Now, programs that need to detect errors will interrogate Error. This is demonstrated in Main( ), where a boundary error is intentionally generated, and the Error property is used to detect it.
Beginning with C# 3.0, it became possible to implement very simple properties without having to explicitly define the variable managed by the property. Instead, you can let the compiler automatically supply the underlying variable. This is called an auto-implemented property. It has the following general form:
type name { get; set; }
Here, type specifies the type of the property and name specifies the name. Notice that get and set are immediately followed by a semicolon. The accessors for an auto-implemented property have no bodies. This syntax tells the compiler to automatically create a storage location (sometimes referred to as a backing field) that holds the value. This variable is not named and is not directly available to you. Instead, it can be accessed only through the property.
Here is how a property called UserCount is declared using an auto-implemented property:
public int UserCount { get; set; }
Notice that no variable is explicitly declared. As explained, the compiler automatically generates an anonymous field that holds the value. Otherwise, UserCount acts like and is used like any other property.
Unlike normal properties, an auto-implemented property cannot be read-only or write-only. Both the get and set must be specified in all cases. However, you can approximate the same effect by declaring either get or set as private, as explained in “Use Access Modifiers with Accessors” later in this chapter.
Although auto-implemented properties offer convenience, their use is limited to those cases in which you do not need control over the getting or setting of the backing field. Remember, you cannot access the backing field directly. This means that there is no way to constrain the value an auto-implemented property can have. Thus, auto-implemented properties simply let the name of the property act as a proxy for the field, itself. However, sometimes this is exactly what you want. Also, they can be very useful in cases in which properties are used to expose functionality to a third party, possibly through a design tool.
As discussed in Chapter 8, an object initializer provides an alternative to explicitly calling a constructor when creating an object. When using object initializers, you specify initial values for the fields and/or properties that you want to initialize. Furthermore, the object initializer syntax is the same for both properties or fields. For example, here is the object initializer demonstration program from Chapter 8, reworked to show the use of object initializers with properties. Recall that the version shown in Chapter 8 used fields. The only difference between this version of the program and the one shown in Chapter 8 is that Count and Str have been converted from fields into properties. The object initializer syntax is unchanged.
// Use object initializers with properties.
using System;
class MyClass {
// These are now properties.
public int Count { get; set; }
public string Str { get; set; }
}
class ObjInitDemo {
static void Main() {
// Construct a MyClass object by using object initializers.
MyClass obj = new MyClass { Count = 100, Str = "Testing" };
Console.WriteLine(obj.Count + " " + obj.Str);
}
}
As you can see, the properties Count and Str are set via object initializer expressions. The output is the same as that produced by the program in Chapter 8 and is shown here:
100 Testing
As explained in Chapter 8, the object initializer syntax is most useful when working with anonymous types generated by a LINQ expression. In most other cases, you will use the normal constructor syntax.
Properties have some important restrictions. First, because a property does not define a storage location, it cannot be passed as a ref or out parameter to a method. Second, you cannot overload a property. (You can have two different properties that both access the same variable, but this would be unusual.) Finally, a property should not alter the state of the underlying variable when the get accessor is called. Although this rule is not enforced by the compiler, violating it is semantically wrong. A get operation should be nonintrusive.
By default, the set and get accessors have the same accessibility as the indexer or property of which they are a part. For example, if the property is declared public, then by default the get and set accessors are also public. It is possible, however, to give set or get its own access modifier, such as private. In all cases, the access modifier for an accessor must be more restrictive than the access specification of its property or indexer.
There are a number of reasons why you may want to restrict the accessibility of an accessor. For example, you might want to let anyone obtain the value of a property, but allow only members of its class to set the property. To do this, declare the set accessor as private. For example, here is a property called MyProp that has its set accessor specified as private.
// Use an access modifier with an accessor.
using System;
class PropAccess {
int prop; // field being managed by MyProp
public PropAccess() { prop = 0; }
/* This is the property that supports access to
the private instance variable prop. It allows
any code to obtain the value of prop, but only
other class members can set the value of prop. */
public int MyProp {
get {
return prop;
}
private set { // now, private
prop = value;
}
}
// This class member increments the value of MyProp.
public void IncrProp() {
MyProp++; // OK, in same class.
}
}
// Demonstrate accessor access modifier.
class PropAccessDemo {
static void Main() {
PropAccess ob = new PropAccess();
Console.WriteLine("Original value of ob.MyProp: " + ob.MyProp);
// ob.MyProp = 100; // can't access set
ob.IncrProp();
Console.WriteLine("Value of ob.MyProp after increment: "
+ ob.MyProp);
}
}
In the PropAccess class, the set accessor is specified private. This means that it can be accessed by other class members, such as IncrProp( ), but it cannot be accessed by code outside of PropAccess. This is why the attempt to assign ob.MyProp a value inside PropAccessDemo is commented out.
Perhaps the most important use of restricting an accessor’s access is found when working with auto-implemented properties. As explained, it is not possible to create a read-only or write-only auto-implemented property because both the get and set accessors must be specified when the auto-implemented property is declared. However, you can gain much the same effect by declaring either get or set as private. For example, this declares what is effectively a read-only, auto-implemented Length property for the FailSoftArray class shown earlier.
public int Length { get; private set; }
Because set is private, Length can be set only by code within its class. Outside its class, an attempt to change Length is illegal. Thus, outside its class, Length is effectively read-only. The same technique can also be applied to the Error property, like this:
public bool Error { get; private set; }
This allows Error to be read, but not set, by code outside FailSoftArray.
To try the auto-implemented version of Length and Error with FailSoftArray, first remove the len and ErrFlag variables. They are no longer needed. Then, replace each use of len inside FailSoftArray with Length and each use of ErrFlag with Error. Here is the updated version of FailSoftArray along with a Main( ) method to demonstrate it:
// Use read-only, auto-implemented properties for Length and Error.
using System;
class FailSoftArray {
int[] a; // reference to underlying array
// Construct array given its size.
public FailSoftArray(int size) {
a = new int[size];
Length = size;
}
// An auto-implemented, read-only Length property.
public int Length { get; private set; }
// An auto-implemented, read-only Error property.
public bool Error { get; private set; }
// This is the indexer for FailSoftArray.
public int this[int index] {
// This is the get accessor.
get {
if(ok(index)) {
Error = false;
return a[index];
} else {
Error = true;
return 0;
}
}
// This is the set accessor.
set {
if(ok(index)) {
a[index] = value;
Error = false;
}
else Error = true;
}
}
// Return true if index is within bounds.
private bool ok(int index) {
if(index >= 0 & index < Length) return true;
return false;
}
}
// Demonstrate the improved fail-soft array.
class FinalFSDemo {
static void Main() {
FailSoftArray fs = new FailSoftArray(5);
// Use Error property.
for(int i=0; i < fs.Length + 1; i++) {
fs[i] = i*10;
if(fs.Error)
Console.WriteLine("Error with index " + i);
}
}
}
This version of FailSoftArray works the same as the previous version, but it does not contain the explicitly declared backing fields.
Here are some restrictions that apply to using access modifiers with accessors. First, only the set or get accessor can be modified, not both. Furthermore, the access modifier must be more restrictive than the access level of the property or indexer. Finally, an access modifier cannot be used when declaring an accessor within an interface or when implementing an accessor specified by an interface. (Interfaces are described in Chapter 12.)
Although the preceding examples have demonstrated the basic mechanism of indexers and properties, they haven’t displayed their full power. To conclude this chapter, a class called RangeArray is developed that uses indexers and properties to create an array type in which the index range of the array is determined by the programmer.
As you know, in C# all arrays begin indexing at zero. However, some applications would benefit from an array that allows indexes to begin at any arbitrary point. For example, in some situations it might be more convenient for an array to begin indexing with 1. In another situation, it might be beneficial to allow negative indexes, such as an array that runs from –5 to 5. The RangeArray class developed here allows these and other types of indexing.
Using RangeArray, you can write code like this:
RangeArray ra = new RangeArray(-5, 10); // array with indexes from -5 to 10
for(int i=-5; i <= 10; i++) ra[i] = i; // index from -5 to 10
As you can guess, the first line constructs a RangeArray that runs from –5 to 10, inclusive. The first argument specifies the beginning index. The second argument specifies the ending index. Once ra has been constructed, it can be indexed from –5 to 10.
The entire RangeArray class is shown here, along with RangeArrayDemo, which demonstrates the array. As implemented here, RangeArray supports arrays of int, but you can change the data type, if desired.
/* Create a specifiable range array class.
The RangeArray class allows indexing to begin at
some value other than 0. When you create a RangeArray,
you specify the beginning and ending index. Negative
indexes are also allowed. For example, you can create
arrays that index from -5 to 5, 1 to 10, or 50 to 56.
*/
using System;
class RangeArray {
// Private data.
int[] a; // reference to underlying array
int lowerBound; // smallest index
int upperBound; // largest index
// An auto-implemented, read-only Length property.
public int Length { get; private set; }
// An auto-implemented, read-only Error property.
public bool Error { get; private set; }
// Construct array given its size.
public RangeArray(int low, int high) {
high++;
if(high <= low) {
Console.WriteLine("Invalid Indices");
high = 1; // create a minimal array for safety
low = 0;
}
a = new int[high - low];
Length = high - low;
lowerBound = low;
upperBound = --high;
}
// This is the indexer for RangeArray.
public int this[int index] {
// This is the get accessor.
get {
if(ok(index)) {
Error = false;
return a[index - lowerBound];
} else {
Error = true;
return 0;
}
}
// This is the set accessor.
set {
if(ok(index)) {
a[index - lowerBound] = value;
Error = false;
}
else Error = true;
}
}
// Return true if index is within bounds.
private bool ok(int index) {
if(index > = lowerBound & index <= upperBound) return true;
return false;
}
}
// Demonstrate the index-range array.
class RangeArrayDemo {
static void Main() {
RangeArray ra = new RangeArray(-5, 5);
RangeArray ra2 = new RangeArray(1, 10);
RangeArray ra3 = new RangeArray(-20, -12);
// Demonstrate ra.
Console.WriteLine("Length of ra: " + ra.Length);
for(int i = -5; i <= 5; i++)
ra[i] = i;
Console.Write("Contents of ra: ");
for(int i = -5; i <= 5; i++)
Console.Write(ra[i] + " ");
Console.WriteLine("\n");
// Demonstrate ra2.
Console.WriteLine("Length of ra2: " + ra2.Length);
for(int i = 1; i <= 10; i++)
ra2[i] = i;
Console.Write("Contents of ra2: ");
for(int i = 1; i <= 10; i++)
Console.Write(ra2[i] + " ");
Console.WriteLine("\n");
// Demonstrate ra3.
Console.WriteLine("Length of ra3: " + ra3.Length);
for(int i = -20; i <= -12; i++)
ra3[i] = i;
Console.Write("Contents of ra3: ");
for(int i = -20; i <= -12; i++)
Console.Write(ra3[i] + " ");
Console.WriteLine("\n");
}
}
The output from the program is shown here:
Length of ra: 11
Contents of ra: -5 -4 -3 -2 -1 0 1 2 3 4 5
Length of ra2: 10
Contents of ra2: 1 2 3 4 5 6 7 8 9 10
Length of ra3: 9
Contents of ra3: -20 -19 -18 -17 -16 -15 -14 -13 -12
As the output verifies, objects of type RangeArray can be indexed in ways other than starting at zero. Let’s look more closely at how RangeArray is implemented.
RangeArray begins by defining the following private instance variables:
// Private data.
int[] a; // reference to underlying array
int lowerBound; // smallest index
int upperBound; // largest index
The underlying array is referred to by a. This array is allocated by the RangeArray constructor. The index of the lower bound of the array is stored in lowerBound, and the index of the upper bound is stored in upperBound.
Next, the auto-implemented, read-only properties Length and Error are declared:
// An auto-implemented, read-only Length property.
public int Length { get; private set; }
// An auto-implemented, read-only Error property.
public bool Error { get; private set; }
Notice that for both properties, the set accessor is private. As explained earlier in this chapter, this results in what is effectively a read-only, auto-implemented property.
The RangeArray constructor is shown here:
// Construct array given its size.
public RangeArray(int low, int high) {
high++;
if(high <= low) {
Console.WriteLine("Invalid Indices");
high = 1; // create a minimal array for safety
low = 0;
}
a = new int[high - low];
Length = high - low;
lowerBound = low;
upperBound = --high;
}
A RangeArray is constructed by passing the lower bound index in low and the upper bound index in high. The value of high is then incremented because the indexes specified are inclusive. Next, a check is made to ensure that the upper index is greater than the lower index. If not, an error is reported and a one-element array is created. Next, storage for the array is allocated and assigned to a. Then the Length property is set equal to the number of elements in the array. Finally, lowerBound and upperBound are set.
Next, RangeArray implements its indexer, as shown here:
// This is the indexer for RangeArray.
public int this[int index] {
// This is the get accessor.
get {
if(ok(index)) {
Error = false;
return a[index - lowerBound];
} else {
Error = true;
return 0;
}
}
// This is the set accessor.
set {
if(ok(index)) {
a[index - lowerBound] = value;
Error = false;
}
else Error = true;
}
}
This indexer is similar to the one used by FailSoftArray, with one important exception. Notice the expression that indexes a. It is
index - lowerBound
This expression transforms the index passed in index into a zero-based index suitable for use on a. This expression works whether lowerBound is positive, negative, or zero.
The ok( ) method is shown here:
// Return true if index is within bounds.
private bool ok(int index) {
if(index >= lowerBound & index <= upperBound) return true;
return false;
}
It is similar to the one used by FailSoftArray except that the range is checked by testing it against the values in lowerBound and upperBound.
RangeArray illustrates just one kind of custom array that you can create through the use of indexers and properties. There are, of course, several others. For example, you can create dynamic arrays, which expand and contract as needed, associative arrays, and sparse arrays. You might want to try creating one of these types of arrays as an exercise.