In this article, we are going to learn about Indexers in C#, how to declare them in a class, struct, or interface, and overload them. We’re also going to learn what the differences between indexers and properties are.

To download the source code for this article, you can visit our GitHub repository.

Let’s start.

What Are Indexers and What Do Indexers in C# Do?

We’re all used to dealing with arrays. The array is easy to use and most importantly its elements are indexed, i.e. we can access any element by its index. But what if we want to add indexing to our class or struct?

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

This is precisely what the Indexers achieve for us.

Indexer allows instances of a class or struct to have indexes. It enables us to do things that arrays alone cannot, and we’re going to see what exactly these things are going forward.

How to Implement Indexers in C#

Let’s start by checking out the Indexers syntax. We can define Indexer by using the this keyword:

[acccess modifier] [return type] this([parameters])
{
    get 
    { 
        //return a value 
    }
    set 
    { 
        //set a value
    }
}

access modifier: Can be public, private, protected, or internal.

return type: Can be any type we need, it certainly can’t be void.

parameters: Can be any type and not just int type as in arrays. We should define one parameter at least.

get accessor: Returns a value.

set accessor: Assigns a value.

We should define either set or get accessor at least.

Expression Body Definitions

When the body of the get accessor is a single statement, we can use lambda (=>) expression.

Beginning with C# 7.0, we can also use this expression with the set accessor:

[acccess modifier] [return type] this([parameters])
{
    get => // single statment
    set => // single statment
}

Create One-Dimensional Arrays With Indexers

When the indexer has only one parameter, this is similar to one-dimensional arrays:

public class Sentence
{
    private string[] _words;
    public int Length => _words.Length; 
    
    public Sentence(string sentence)
    {
        _words = sentence.Split(' ');
    }

    public string this[int wordNumber]
    {
        get
        {
            return _words[wordNumber];
        }
        set
        {
            _words[wordNumber] = value;
        }
    }
}

We create a Sentence class, it contains a _words array and the Indexer takes an int type as a parameter, its modifier is public and the return type is string.

In the constructor, we split the sentence into words to fill the _words array.

Now, we can use the Sentence class as an array:

var sentence = new Sentence("Hello from Code Maze");
for (int i = 0; i < sentence.Length; i++)
{
    Console.WriteLine("Word {0} : {1}", i + 1, sentence[i]);
}

// The output is:
// Word 1 : Hello
// Word 2 : from 
// Word 3 : Code 
// Word 4 : Maze

So far this looks pretty similar to what we can do with arrays, so what is the difference between indexers and arrays exactly here?

One of the advantages of Indexers is that we can add validation to the input:

public class Sentence
{
    private string[] _words;
    public int Length => _words.Length;

    public Sentence(string sentence)
    {
        if (sentence == null)
            throw new ArgumentNullException(nameof(sentence));

        _words = sentence.Split(' ');
    }

    public string this[int wordNumber]
    {
        get
        {
            if (wordNumber < 0 || wordNumber > _words.Length - 1)
                throw new ArgumentOutOfRangeException(nameof(wordNumber));

            return _words[wordNumber];
        }
        set
        {
            if (wordNumber < 0 || wordNumber > _words.Length - 1)
                throw new ArgumentOutOfRangeException(nameof(wordNumber));

            if (value == null || value.Trim().Length == 0 || value.Split(' ').Length > 1)
                return;

            _words[wordNumber] = value;
        }
    }
}

In the constructor, we add a condition to check if sentence is null.

In the get accessor we make sure that the wordNumber is within the allowed range and in the set accessor we do this too and in addition, we make sure the value is valid.

Create Multi-Dimensional Arrays With Indexers

As we’ve mentioned, we can pass more than one parameter to Indexers, similarly to how we do it with multi-dimensional arrays.

A good example of this is the Tic-Tac-Toe game.

First, let’s create an enum that contains our CellStatus:

public enum CellStatus
{
    Empty,
    X,
    O
}

Then, let’s create TicTacToe class:

public class TicTacToe
{
    private const int _rowCount = 3;
    private const int _colCount = 3;
    private CellStatus[,] _patch = new CellStatus[_rowCount, _colCount];
    public CellStatus this[int row, int col]
    {
        get
        {
            if (row >= _rowCount || row < 0)
                throw new ArgumentOutOfRangeException(nameof(row));
            if (col >= _colCount || col < 0)
                throw new ArgumentOutOfRangeException(nameof(col));

            return _patch[row, col];
        }
        set
        {
            if (row >= _rowCount || row < 0)
                throw new ArgumentOutOfRangeException(nameof(row));
            if (col >= _colCount || col < 0)
                throw new ArgumentOutOfRangeException(nameof(col));
            if (!Enum.IsDefined(value))
                return;
            if (value == CellStatus.Empty)
                return;
            if (_patch[row, col] != CellStatus.Empty)
                return;
            _patch[row, col] = value;
        }
    }

    public override string ToString()
    {
        string s = "";
        for (int i = 0; i < _rowCount; i++)
        {
            for (int j = 0; j < _colCount; j++)
            {
                s += _patch[i, j].ToString();
                s += "\t";
            }
            s += Environment.NewLine;
        }

        return s;
    }
}

Similar to the previous example, here we have two parameters representing row number and column number, respectively.

We can use the TicTacToe class as a two-dimensional array:

var ticTacToe = new TicTacToe();
ticTacToe[0, 0] = CellStatus.X;
Console.WriteLine(ticTacToe.ToString());

// The output is:
// X Empty Empty
// Empty Empty Empty
// Empty Empty Empty

Overloaded Indexers

Indexers can also be overloaded, i.e we can define more than one indexer in our class or our struct.

Let’s apply this to the previous example, we want access to the cell by one integer index representing a cell number.

Cell numbering begins with 1, and ends with 9, where cell(0,0) becomes cell(1), cell(0,1) becomes cell(2), etc ..

We will add a simple method to archive a conversion process:

private static (int, int) Convert(int cellNumber)
{
    if(cellNumber <= 0)
        throw new ArgumentOutOfRangeException(nameof(cellNumber));

    return ((cellNumber - 1) / _rowCount, (cellNumber - 1) % _colCount);
}

And we let’s add another Indexer with one parameter:

public CellStatus this[int cellNumber]
{
    get
    {
        var result = Convert(cellNumber);
        return this[result.Item1, result.Item2];
    }
    set
    {
        var result = Convert(cellNumber);
        this[result.Item1, result.Item2] = value;
    }
}

We just convert the cell number to a row and a column and then call the other indexer. 

Now, we can use TicTacToe class with one index:

ticTacToe[9] = CellStatus.O;
Console.WriteLine(ticTacToe.ToString());

// The output is:
// X Empty Empty
// Empty Empty Empty
// Empty Empty O

Indexers in Interfaces

If we need to, we can also declare indexers in interfaces the same way we do in classes, except interface accessors don’t have modifiers:

public interface IIndexerInterface
{ 
    string this[int index] { get; }
}

public class IndexerClass : IIndexerInterface
{
    public string this[int index] 
    { 
        get => "Hello from class."; 
    }
}

We notice that a get interface accessor does not have a body but after a default interface methods feature in C# 8.0, we can write a default body.

Let’s add another indexer with a default implementation:

string this [string name] 
{
    get => "Hello from interface.";
}

This indexer does not need to be implemented in IndexerClass class because the get accessor has a default body.

Comparison Between Properties and Indexers in C#

Indexers resemble properties but there are some differences between them:

IndexerProperty
Defined by this keyword.Defined by a name.
Accessed by an index.Accessed by a name.
Can be only an instance member.Can be a static or an instance member.
A get accessor has the same formal parameter list
as the indexer
A get accessor has no parameters.
A set accessor has the same formal parameter list
as the indexer, and also to the value parameter.
A set accessor contains the implicit value parameter.

When We Should Use Indexers?

We should use Indexers when our class, struct, or interface has an internal collection that needs to be encapsulated and accessed as an array. This way we can use our class/struct as a data structure.

Besides the convenience and simplified syntax, Indexers provide us with more access flexibility and more features than arrays do, like the validation for example.

Conclusion

In this article, we’ve learned a lot about Indexers in C#. We have seen how to define single or multi-dimensional indexers and how overloaded them and we have compared them with the properties.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!