Providing an Indexer

Arrays, dictionaries, and lists all provide an indexer as a convenient way to get or set a member of a collection based on a key or index. As we’ve seen, to use the indexer, you simply put the index (or indices) in square brackets after the collection name. It is possible to define your own indexer; Listing 17.10 shows an example using Pair<T>.

Listing 17.10: Defining an Indexer
1. interface IPair<T>
2. {
3.     T First { get; }
4.     T Second { get; }
5.     T this[PairItem index] { get; }
6. }
7.  
8. public enum PairItem
9. {
10.     First,
11.     Second
12. }
13.  
14. public struct Pair<T> : IPair<T>
15. {
16.     public Pair(T first, T second)
17.     {
18.         First = first;
19.         Second = second;
20.     }
21.  
22.     public T First { get; }
23.     public T Second { get; }
24.  
25.     public T this[PairItem index]
26.     {
27.         get
28.         {
29.             switch (index)
30.             {
31.                 case PairItem.First:
32.                     return First;
33.                 case PairItem.Second:
34.                     return Second;
35.                 default:
36.                     throw new NotImplementedException(
37.                         $"The enum { index.ToString() } has not been implemented");
38.  
39.             }
40.         }
41.         // ...
42.     }
43. }

An indexer is declared much as a property is declared, except that instead of the name of the property, you use the keyword this followed by a parameter list in square brackets. The body is also like a property, with get and set blocks. As Listing 17.10 shows, the parameter does not have to be an int. In fact, the index can take multiple parameters and can even be overloaded. This example uses an enum to reduce the likelihood that callers will supply an index for a nonexistent item.

The Common Intermediate Language (CIL) code that the C# compiler creates from an index operator is a special property called Item that takes an argument. Properties that accept arguments cannot be created explicitly in C#, so the Item property is unique in this aspect. Any additional member with the identifier Item, even if it has an entirely different signature, will conflict with the compiler-created member, so it will not be allowed.

AdVanced Topic
Assigning the Indexer Property Name Using IndexerName

As indicated earlier, the CIL property name for an indexer defaults to Item. Using the IndexerNameAttribute, you can specify a different name, however. Listing 17.11, for example, changes the name to "Entry".

Listing 17.11: Changing the Indexer’s Default Name
1. [System.Runtime.CompilerServices.IndexerName("Entry")]
2. public T this[PairItem index]
3. {
4.     // ...
5. }

This makes no difference to C# callers of the index, but it specifies the name for languages that do not support indexers directly.

The IndexerNameAttribute is merely an instruction to the compiler to use a different name for the indexer; the attribute is not actually emitted into metadata by the compiler, so it is not available via reflection.

AdVanced Topic
Defining an Index Operator with Variable Parameters

An index operator can also take a variable parameter list. For example, Listing 17.12 defines an index operator for BinaryTree<T>, discussed in Chapter 12 (and again in the next section).

Listing 17.12: Defining an Index Operator with Variable Parameters
1. using System;
2.  
3. public class BinaryTree<T> 
4. {
5.     // ...
6.     public BinaryTree<T> this[params PairItem[]? branches]
7.     {
8.         get
9.         {
10.             BinaryTree<T> currentNode = this;
11.  
12.             // Allow either an empty array or null
13.             // to refer to the root node
14.             int totalLevels = branches?.Length ?? 0;
15.             int currentLevel = 0;
16.  
17.             while (currentLevel < totalLevels)
18.             {
19.                 System.Diagnostics.Debug.Assert(branches is not null,
20.                     $"nameof(branches) } is not null");
21.  
22.                 currentNode = currentNode.SubItems[
23.                     branches[currentLevel]];
24.                 if (currentNode is null)
25.                 {
26.                     // The binary tree at this location is null
27.                     throw new IndexOutOfRangeException();
28.                 }
29.                 currentLevel++;
30.             }
31.             return currentNode;
32.         }
33.     }
34.     // ...
35. }

Each item within branches is a PairItem and indicates which branch to navigate down in the binary tree. For example,

tree[PairItem.Second, PairItem.First].Value

will retrieve the value located at the second item in the first branch, followed by the first branch within that branch.

{{ snackbarMessage }}
;