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.
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?
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:
Indexer | Property |
---|---|
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.