There’s a feature in C# that’s very useful for some use cases, yet many people seem to overlook it. I’m talking about the indexer operator. What this operator does is, it makes it possible to access elements of an enumerable data structure that your class makes use of, with the familiar [] operator.
Let’s say we have a class named Car:
public class Car | |
{ | |
public string Make { get; set; } | |
public string Model { get; set; } | |
public int Year { get; set; } | |
public bool Clean { get; set; } | |
public int DamageAmount { get; set; } | |
} |
Now let’s say that we have multiple cars, multiple instances of the class Car and that we want to organize them in a collection. What should we use? A plain old Generic List you might say and you would be correct, but what happens if we want to augment that list with more operations, more car-specific operations. Perhaps we want to clean the cars, or repair them. All that logic can nicely go inside a class named Garage:
As you can see we still use a List
public class Garage | |
{ | |
/// <summary> | |
/// Adds a car to the garage | |
/// </summary> | |
/// <param name="car">The car to be added to the garage</param> | |
public void Add(Car car) | |
{ | |
//Searches the list for any occurrences of the given car | |
if (cars.FindAll((c) => { return c.Equals(car); }).Count == 0) | |
{ | |
cars.Add(car); | |
} | |
} | |
/// <summary> | |
/// Returns the car at the specified index | |
/// </summary> | |
/// <param name="index"></param> | |
/// <returns></returns> | |
public Car Get(int index) | |
{ | |
return cars.ElementAt(index); | |
} | |
/// <summary> | |
/// Removes a car from the garage | |
/// </summary> | |
/// <param name="car">The instance of the car to be removed from the garage</param> | |
public void Remove(Car car) | |
{ | |
cars.Remove(car); | |
} | |
/// <summary> | |
/// The number of cars in the garage | |
/// </summary> | |
public int Count | |
{ | |
get { return cars.Count; } | |
} | |
protected List<Car> cars = new List<Car>(); | |
} |
This is an initial draft of our garage class showing the use of the delegate pattern. We can also include other stuff to make the class resemble real-life garage even more
public class Garage | |
{ | |
/// <summary> | |
/// Adds a car to the garage | |
/// </summary> | |
/// <param name="car">The car to be added to the garage</param> | |
public void Add(Car car) | |
{ | |
//Searches the list for any occurrences of the given car | |
if (cars.FindAll((c) => { return c.Equals(car); }).Count == 0) | |
{ | |
cars.Add(car); | |
} | |
} | |
/// <summary> | |
/// Returns the car at the specified index | |
/// </summary> | |
/// <param name="index"></param> | |
/// <returns></returns> | |
public Car Get(int index) | |
{ | |
return cars.ElementAt(index); | |
} | |
/// <summary> | |
/// Removes a car from the garage | |
/// </summary> | |
/// <param name="car">The instance of the car to be removed from the garage</param> | |
public void Remove(Car car) | |
{ | |
cars.Remove(car); | |
} | |
/// <summary> | |
/// The number of cars in the garage | |
/// </summary> | |
public int Count | |
{ | |
get { return cars.Count; } | |
} | |
public void Clean() | |
{ | |
foreach(var car in cars) | |
{ | |
car.Clean = true; | |
} | |
} | |
public void Repair() | |
{ | |
foreach(var car in cars) | |
{ | |
car.DamageAmount = 0; | |
} | |
} | |
protected List<Car> cars = new List<Car>(); | |
} |
The addition of the Clean() and Repair() methods improves our Garage implementation but there’s something missing. Every time we want to access a specific car by index we are forced to use the Get() method. It’s counter-intuitive. Here’s where the powerful indexer operator comes into play. After we’ve implemented an indexer operator in our collection class we will be able to access car elements with array-like syntax, like this:
Garage garage = new Garage(); | |
garage.Add(new Car() | |
{ | |
Make = "Porsche", | |
Model = "Carrera GT", | |
Year = 2011, | |
Clean = true, | |
DamageAmount = 27 | |
}); | |
garage.Add(new Car() | |
{ | |
Make = "Toyota", | |
Model = "Yaris", | |
Year = 2015, | |
Clean = true, | |
DamageAmount = 0 | |
}); | |
garage.Add(new Car() | |
{ | |
Make = "Ford", | |
Model = "Mustang GT", | |
Year = 2010, | |
Clean = false, | |
DamageAmount = 68 | |
}); | |
var cityCar = garage[1]; //Toyota Yaris |
Let’s see how simple it is to do that
public class Garage | |
{ | |
/// <summary> | |
/// Adds a car to the garage | |
/// </summary> | |
/// <param name="car">The car to be added to the garage</param> | |
public void Add(Car car) | |
{ | |
//Searches the list for any occurrences of the given car | |
if (cars.FindAll((c) => { return c.Equals(car); }).Count == 0) | |
{ | |
cars.Add(car); | |
} | |
} | |
/// <summary> | |
/// Returns the car at the specified index | |
/// </summary> | |
/// <param name="index"></param> | |
/// <returns></returns> | |
public Car Get(int index) | |
{ | |
return cars.ElementAt(index); | |
} | |
/// <summary> | |
/// Removes a car from the garage | |
/// </summary> | |
/// <param name="car">The instance of the car to be removed from the garage</param> | |
public void Remove(Car car) | |
{ | |
cars.Remove(car); | |
} | |
/// <summary> | |
/// The number of cars in the garage | |
/// </summary> | |
public int Count | |
{ | |
get { return cars.Count; } | |
} | |
public void Clean() | |
{ | |
foreach(var car in cars) | |
{ | |
car.Clean = true; | |
} | |
} | |
public void Repair() | |
{ | |
foreach(var car in cars) | |
{ | |
car.DamageAmount = 0; | |
} | |
} | |
public Car this[int index] | |
{ | |
get | |
{ | |
return Get(index); | |
} | |
} | |
protected List<Car> cars = new List<Car>(); | |
} |
So basically, you override the indexer operator like a property, with a getter and a setter. In this particular example we don’t want to be able to manipulate the contents of the garage so we don’t need a setter. But using a setter is a totally legitimate operation and one you will probably find pretty helpful once you start using it. That’s how easy it is
Check the whole project on this GitHub repo