What are Indexers in C#?
Indexers allows any custom class or struct to offer array like storage. Just like in arrays, we use indexer ( [ ] ) to access it’s element, we can have that for our custom class too. The implementation is very similar like implementing properties, except that we don’t have to provide index in property. Let’s see by an example.
Consider following scenarios. We have a class called Application
. An application can have references and it should not be more than 3. Let’s see the implementation:
using System;
class Reference<T>
{
private T[] _refearr = new T[3];
private int nextIndex = 0;
//This is indexer
public T this[int i]
{
get => _refearr[i];
set
{
if (nextIndex >= _refearr.Length)
throw new IndexOutOfRangeException($"You can only provide {_refearr.Length} references.");
_refearr[nextIndex++] = value;
}
}
}
class Application
{
public string Name { get; set; }
public Reference<string> AppReference { get; set; }
public Application()
{
AppReference = new Reference<string>();
}
}
class Program
{
static void Main()
{
var myApplication = new Application();
myApplication.Name = "Member Application";
myApplication.AppReference[0] = "A123";
myApplication.AppReference[1] = "B123";
myApplication.AppReference[2] = "C123";
Console.WriteLine(myApplication.AppReference[0]); // Output: A123
}
}
See the Reference
class. It has built in limit of 3 members and we have defined array using private T[] _refearr = new T[3];
. The indexer is defined as public T this[int i]
. It also is a generic class with simple get
and set
accessor methods to assign and retrieve values. The generic will make Reference
more flexible for other requirements, like storing int
, float
etc.
On set
, we are checking the max number of elements and throwing exception accordingly. This is very important to handle the scenario where a user can accidentally create four or more references. As is, indexer set
must have some kind of validation as it’s based on array.
Further, you can’t have two indexers in a same class of same type. Meaning, if you modify Reference class as below then it will throw an error.
class Reference<T>
{
private T[] _refearr = new T[3];
private int nextIndex = 0;
//This is indexer
public T this[int i]
{
get => _refearr[i];
set
{
if (nextIndex >= _refearr.Length)
throw new IndexOutOfRangeException($"You can only provide {_refearr.Length} references.");
_refearr[nextIndex++] = value;
}
}
//This is second indexer
public T this[int i]
{
/* get and set implementation */
}
}
C# compiler will throw an error for above code as both the indexers have same signature. To resolve this issue, we can defined it like this:
class Reference<T>
{
private T[] _refearr = new T[3];
private int nextIndex = 0;
//This is indexer
public T this[int i]
{
get => _refearr[i];
set
{
if (nextIndex >= _refearr.Length)
throw new IndexOutOfRangeException($"You can only provide {_refearr.Length} references.");
_refearr[nextIndex++] = value;
}
}
//This is second Valid indexer
public T this[string i]
{
/* get and set implementation */
}
}
Indexer in Interface
We can also declare indexer in an interface. As we know, interface itself doesn’t have any implementation and is merely a template, indexer will help users to define their own version of the indexer. Let’s look at an example.
using System;
using System.Collections.Generic;
public interface IPhoneBook
{
string this[string name] { get; set; } // This is indexer in interface
void AddContact(string name, string phoneNumber);
}
public class PhoneBook : IPhoneBook
{
private readonly Dictionary<string, string> _contacts = new Dictionary<string, string>();
public string this[string name]
{
get => _contacts.ContainsKey(name) ? _contacts[name] : "Contact not found";
set => _contacts[name] = value;
}
public void AddContact(string name, string phoneNumber)
{
_contacts[name] = phoneNumber;
}
}
class Program
{
static void Main()
{
IPhoneBook phoneBook = new PhoneBook();
// Adding contacts
phoneBook.AddContact("John Doe", "555-1234");
phoneBook.AddContact("Jane Smith", "555-5678");
// Accessing contacts using the indexer
Console.WriteLine(phoneBook["John Doe"]); // Output: 555-1234
Console.WriteLine(phoneBook["Jane Smith"]); // Output: 555-5678
Console.WriteLine(phoneBook["Unknown"]); // Output: Contact not found
// Updating a contact using the indexer
phoneBook["John Doe"] = "555-0000";
Console.WriteLine(phoneBook["John Doe"]); // Output: 555-0000
}
}
Output:
555-1234
555-5678
Contact not found
555-0000
Here, we have developed a phone book application where users can look up contact details by name. We want to create an interface that enforces any class implementing it to support indexing by contact name.
As you can see, we haven’t created any GetContact
methods, this is because, indexer will help us to retrieve it.
Conclusion
Indexers allow objects to be accessed like arrays, where a get
accessor retrieves a value, and a set
accessor assigns one. The indexing mechanism isn’t limited to integers—you can define it however you like. Indexers can be overloaded and can also accept multiple parameters, such as when working with a two-dimensional array.