Indexers

Indexers

The Crash Course

  • An indexer is a way to essentially overload the indexing operator ('[' and ']) for a class.
  • Defining an indexer works in a way that feels like a cross between operator overloading and a property: public double this[int index] { get { /* get code here */ } set { /* set code here */ } }
  • You don't just have to use an int as an index. You can use any type you want.
  • You can have multiple indices by simply listing multiple things where you define the indexer. So instead of [int index], you could have [string someTextIndex, int numericIndex].

Introduction

In the previous tutorial, we talked about how to overload operators. In this tutorial, we'll talk about indexers, which are essentially the equivalent of overloading the indexing operator ('[' and ']').

Like with operator overloading, it is worth cautioning you that just because you can do indexing doesn't mean it is always appropriate.

How to Make an Indexer

Defining an indexer is almost like a cross between overloading an operator and a property. It is pretty easy to do, and so I'm just going to start with showing you some code that does this. This code could be added into the Vector class that we made in the last tutorial, though if you simply modify what the get and set accessors do, you can add it into any class.

public double this[int index]
{
    get
    {
        if(index == 0) { return this.X; }
        else if(index == 1) { return this.Y; }
        else { throw new IndexOutOfRangeException(); }
    }
    set
    {
        if (index == 0) { this.X = value; }
        else if (index == 1) { this.Y = value; }
        else { throw new IndexOutOfRangeException(); }
    }
}

So here, we first specify the access level of the indexer—public in our case, along with the type that it returns. We then use the this keyword, and the square brackets ('[' and ']') that indicate indexing. Inside of the brackets we list the type and name of the indexing variable that we'll use inside of this indexer.

Then, like a property, we have a get and a set block. Note that, also like a property, we do not need to have both of these if we don't want. We can just stick with one of them. Inside of the get and set blocks, we can use our index variable, and like with properties, we can use the value keyword in the setter to refer to the value that is being assigned.

In this little example, I'm making it so that people can refer to the x and y components of the vector using the 0 and 1 index respectively. With this in place, we would now be able to do this:

Vector v = new Vector(5, 2);
double xComponent = v[0]; // we can now use the indexing operator!
double yComponent = v[1];

This, by the way, is much, much easier to understand than what our alternative would have been, which would be to have a GetIndex method, or something like that: xComponent = v.GetIndex(0);.

But wait! There's more! We're not stuck with just using ints as an index. We could use strings, doubles, or anything else we can dream up. (In fact, in the tutorial on using generics, we saw the Dictionary class that does this.)

Here's an alternate version, using strings to do indexing:

public double this[string component]
{
    get
    {
        if (component == "x") { return this.X; }
        else if (component == "y") { return this.Y; }
        else { throw new IndexOutOfRangeException(); }
    }
    set
    {
        if (component == "x") { this.X = value; }
        else if (component == "y") { this.Y = value; }
        else { throw new IndexOutOfRangeException(); }
    }
}

This code is very similar to what we just saw, but this time, we're using a string for indexing. If they ask for "x", we return the x-component. If they ask for "y", we return the y-component.

So now we'd be able to do this:

Vector v = new Vector(5, 2);
double xComponent = v["x"]; // we can now use the indexing operator!
double yComponent = v["y"];

There's still more! We can do indexing with multiple indexes. Now, before I go into this, I want to say this again: just because we can do this, doesn't mean we should. But there are times where this is appropriate, and in those cases, by all means, do it.

Adding in multiple indices is as simple as listing all of them inside of the square brackets when you define your indexer:

public double this[string component, int index]
{
    get
    {
        // Uh... I don't even know what to do with the two indices here.
        // I guess I'll just thrown an exception.  If it makes sense to
        // make an indexer with multiple indices, then you'd write the code
        // to do something intelligent with it here.
        throw new Exception();
    }
    set
    {
        // Uh... I don't even know what to do with the two indices here.
        // I guess I'll just thrown an exception.  If it makes sense to
        // make an indexer with multiple indices, then you'd write the code
        // to do something intelligent with it here.
        throw new Exception();
    }
}

So, yeah, it doesn't make a whole lot of sense, in this particular case, but it shows that it is possible to do, and if you have a use for it, you can make it happen.

What's Next?

In the previous tutorial, we looked at how to overload operators, making most operators look and feel more natural when working with our own, custom-made classes. In this tutorial, we've done the same thing with the indexing operator. In the next tutorial, we'll do the same thing with user-defined conversions—which makes it so we can indicate how a class should get casted (or converted) from your class's type to another.