Using Indexers

Using Indexers

As mentioned, indexers are used to provide index-like access to an object. In C++, this effect is achieved by overloading operator []; however, the C# language was designed to offer a wider range of functionality than the simple [] operator overloading offered by C++. In this section, we’ll examine the syntax used to create indexers using C# and look at the scenarios where it makes sense to supply indexers with your classes. We’ll also look at the features offered by C# indexers that make them more compelling than the similar [] operator in C++. As you’ll see later in this chapter, in the section “Providing Multidimensional Indexers,” C# indexers can easily accept multiple index parameters to simulate multidimensional arrays—a feature that’s difficult to achieve in C++.

Indexers are useful in situations in which single objects must appear to be an array of objects. For example, classes that serve as containers for other objects can implement an indexer to provide a natural way to access the contained objects, such as the .NET Framework’s ArrayList class, shown here:

ArrayList train = new ArrayList();

    

string name = train[42].ToString();

The ArrayList class combines the flexibility of a list with the ease of use found in an array. Although the ArrayList class includes more traditional methods and properties to access contained objects, accessing contained objects as if they’re stored in an array often leads to cleaner code.

Declaring an Indexer

Indexers are declared much like properties, except that the declaration includes the this keyword, along with a declaration for the index key, as shown here:

public object this[int key]
{
    get{ return _items[key]; }
    set{ _items[key] = value; }
}

Just like properties, the get and set accessors are used to define the methods that implement the indexer, with the value keyword representing the parameter passed as the new element value to the set accessor. As with properties, a read-only index is defined by omitting the set accessor, as shown here:

public object this[int key]
{
    // Read-only indexer
    get{ return _items[key]; }
}

An indexer declaration can be declared as virtual or abstract by adding the appropriate keyword to the indexer declaration, as follows:

public virtual object this[int key]
{
    get{ return _items[key]; }
    set{ _items[key] = value; }
}

When declaring an indexer as abstract, you must provide empty get and set accessor declarations:

public abstract object this[int key]
{
    get;
    set;
}

As with other abstract declarations, such as properties or methods, a class that contains an abstract indexer must also be marked as abstract.

Overloading Indexers

Just as classes can define multiple properties, they can also define multiple indexers. Although the compiler differentiates properties by their names, it distinguishes between multiple indexers by their signatures. For example, a class can define indexers that use both string or integer keys, as shown here:

// Index by string value.
public object this[string key]
{
    get{ ... }
    set{ ... }
}

// Index by int value.
public object this[int key]
{
    get{ ... }
    set{ ... }
}

Multiple indexers enable the user to select the most convenient indexing type. Given the preceding index definitions, the user could write code such as this:

gradeIndex[42] = 100;
gradeIndex["Nicolette"] = 100;

When multiple indexers are provided for a class, the compiler determines the proper index to invoke based on the signature of the index parameter. When required, the compiler will apply the usual conversion rules for the index parameters. For example, a short integer used as an index argument in the previous example will use the integer indexer, as follows:

short id = 0;
gradeIndex[id] = 75;    // Uses [int]
Providing Multidimensional Indexers

A class also can declare multidimensional indexers, which is useful when you’re modeling tables or other multidimensional structures. An interesting twist to multidimensional indexers is that each index dimension can have a different type, as shown here:

public class TestScore
{
    public object this[string name, int testNumber]
    {
        get
        {
            return GetGrade(name, testNumber);
        }
        set
        {
            SetGrade(name, testNumber, value);
        }
    }
    
    

}

In the preceding code, test results are indexed by student ID and test number. Providing indexes that follow the natural order of included items can make your classes easier to use. To retrieve test scores using the previous example, you can use code like this:

string studentId = "ALI92";
for(testNumber = 0; testNumber < maxTest; ++testNumber)
{
    decimal score = gradeIndex[studentId, testNumber];
    
    

}
Declaring Indexers for Interfaces

In Chapter 2, interfaces were presented as a way to provide a template for behavior that’s implemented by a type. For example, classes that support the dispose pattern implement the IDisposable interface. In addition to methods and properties, interfaces can also define indexers as part of the contract to be implemented by a type. Including an indexer provides a standard method for accessing objects stored by type. For example, the IDictionary interface, implemented by some of the collection classes that will be discussed in Chapter 8, implements an indexer that’s used to access stored objects.

Indexers can be declared as interface members; however, there are some differences in the declaration syntax when compared to an index declared as a class member. An indexer declaration in an interface doesn’t include any access modifiers such as public or private. Because interfaces never include program statements, the get and set accessors have no bodies. An example of an interface declaration that includes an indexer is shown here:

interface IGradeIndex
{
    object this[string name, int testNumber]
    {
        get;
        set;
    }
}

When a class or structure inherits an interface such as IGradeIndex, the access modifier for the index must be specified and the accessors must be implemented, as shown here:

class GradingTable: IGradeIndex
{
    public object this[string name, int testNumber]
    {
        get
        {
            return GetGrade(name, testNumber);
        }
        set
        {
            SetGrade(name, testNumber, value);
        }
    }
    
    

}
An Indexer Example

Let’s look at a concrete example of how an indexer can be used to add value to your classes. In this section, we’ll build an associative array, a data structure that allows strings to be used as index values, like this:

loveInterests["Romeo"] = "Juliet";
loveInterests["Beatrice"] = "Benedick";
loveInterests["Cyrano"] = "Roxanne";

We’ll use an indexer for the associative array class so that each instance of the class can be used much like the built-in C# array type, except that the indexing will be done using strings rather than scalar values. The AssociativeArray class shown here implements an associative array:

public class AssociativeArray
{
    // Create an array and specify its initial size.
    public AssociativeArray(int initialSize)
    {
        _items = new object[initialSize];
    }
    // Declare the indexer used to access individual array items.
    public object this[string key]
    {
        get{ return KeyToObject(key); }
        set{ AddToArray(key, value); }
    }
    // Mimic the Length property found in other .NET arrays.
    public int Length
    {
        get { return _count; }
    }
    // Helper method used to add an item to the array. If the
    // key already exists, the existing item is replaced. If the
    // array is full, the array size is increased.
    protected void AddToArray(string key, object item)
    {
        if(KeyExists(key))
        {
            // Scroll through the item array and replace the
            // existing item associated with the key with the
            // new item.
            for(int n = 0; n < _count; ++n)
            {
                KeyItemPair pair = (KeyItemPair)_items[n];
                if(key == pair.key)
                    _items[n] = new KeyItemPair(key, item);
            }
        }
        else
        {
            if(_count == _items.Length)
            {
                IncreaseCapacity();
            }
            _items[_count] = new KeyItemPair(key, item);
            _count++;
        }
    }
    // Returns true if a specific key exists in the array;
    // otherwise, returns false.
    protected bool KeyExists(string key)
    {
        for(int n = 0; n < _count; ++n)
        {
            KeyItemPair pair = (KeyItemPair)_items[n];
            if(key == pair.key)
                return true;
        }
        return false;
    }
    // Given a key in the array, returns the associated object, or
    // returns null if the key isn't found.
    protected object KeyToObject(string key)
    {
        for(int n = 0; n < _count; ++n)
        {
            KeyItemPair pair = (KeyItemPair)_items[n];
            if(key == pair.key)
                return pair.item;
        }
        return null;
    }
    // Increases the size of the item array.
    protected void IncreaseCapacity()
    {
        int size = _items.Length + 5;
        object [] oldArray = _items;
        _items = new object[size];
        oldArray.CopyTo(_items, 0);
    }
    // The array that stores items in the associative array
    protected object[] _items;
    // The number of items in the array
    protected int _count = 0;
    // A structure that contains the item and key pair stored in
    // each array element
    protected struct KeyItemPair
    {
        public KeyItemPair(string k, object obj)
        {
            key = k;
            item = obj;
        }
        public object item;
        public string key;
    }
}

Internally, objects are stored in _items, an array of objects that’s automatically grown as needed when additional items are added to the array. The AddToArray method is used to add an item to the _items array; if additional storage is required, the IncreaseCapacity method is used to grow the array, preserving any existing items.

The indexer calls the AddToArray method when adding items and uses the KeyToObject method to retrieve items. If the key isn’t found in the array, KeyToObject returns a null value, which the indexer returns to the caller.

The KeyItemPair structure is used to provide mapping between item keys and the objects associated with the keys. This structure is strictly for internal use only and is never exposed outside the AssociativeArray class.

As mentioned, the AssociativeArray class is used much like the array types that are included in the .NET Framework class library. To use this class to store favorite foods, you can use code like this:

static void Main(string[] args)
{
    AssociativeArray foodFavorites = new AssociativeArray(4);
    foodFavorites["Mickey"] = "Risotto with Wild Mushrooms";
    foodFavorites["Ali"] = "Plain Cheeseburger";
    foodFavorites["Mackenzie"] = "Macaroni and Cheese";
    foodFavorites["Rene"] = "Escargots";

    Console.WriteLine(foodFavorites["Ali"]);
    Console.WriteLine(foodFavorites["Mackenzie"]);
    Console.WriteLine(foodFavorites["Mickey"]);
    Console.WriteLine(foodFavorites["Rene"]);
}


Part III: Programming Windows Forms