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, so I'm just going to start by 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 the brackets, we list the type and name of the indexing variable that we'll use inside 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 easier to understand than our alternative, 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 any type we want. (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 we're using a string for indexing this time. 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 throw 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 throw 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 lot of sense in this 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.
Post preview:
Close preview