Skip to content

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Overload Indexers in C#

Jan 8, 2020 • 9 Minute Read

Introduction

In C# we have many different data structures available that allow us to store and retrieve specific data when needed. This guide will discuss indexers, and especially how to overload them the right way. When we talk about indexer overloading, the idea is to force the class to behave like a data structure, keep track of its instances, and allow us to retrieve those instances as we would do with an array or list.

We will first explore the topic of indexers, then turn our sights towards their overloading.

Indexers

This feature in C# allows you to index as class or struc as you would do it with an array. When we define an indexer for a class, we force it to behave like a virtual array. The array access operator, or [], can be used to access instances of a class that implements the indexer. The user is able to get or set the indexed value without pointing to an instance or a type member. The indexers are very similar to properties, but the main difference is that accessors to the indexers will take parameters, while properties cannot.

There is a general or template syntax for this, which looks as follows.

      type this[type_index index]
{
   get
   {
       // get the instance value from index
   }
   set
   {
       // set the instance value at index
   }
}
    

Let's take a practical example.

      using System;

namespace ndexers
{   
    class Program
    {
        class Students
        {            
            private string[] _indexers = new string[10];
            public string this[int index]
            {
                get { return _indexers[index]; }
                set { _indexers[index] = value; }
            }

        }
        static void Main(string[] args)
        {
            Students elementarySchool = new Students();
            elementarySchool[0] = "Daniel";
            elementarySchool[1] = "Florian";
            elementarySchool[2] = "David";
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine($" The student's name : {elementarySchool[i]}");
            }
            Console.ReadKey();
            }
    }
}
    

Executing the app gives us the following output.

      The student's name : Daniel
 The student's name : Florian
 The student's name : David
 The student's name :
 The student's name :
 The student's name :
 The student's name :
 The student's name :
 The student's name :
 The student's name :
    

What happens here? We have a class called Students behaving like a virtual array and allowing up to ten student names to be stored. In the Main, the class is instantiated and three student names are inserted. Then a for loop is used to run through the elements.

If we set or access the following element.

      elementarySchool[10] = "Anya";
    

The result would be the following.

      System.IndexOutOfRangeException: 'Index was outside the bounds of the array.'
    

This is due to the array-like behavior and the fact that in the class it is set to have 10 elements.

Now that we know how to cause our class to behave like an array, we can find out how to overload these indexers and add extra functionality.

Some important points:

  1. There are two types of indexers, one- and two-dimensional.
  2. Indexers can be overloaded.
  3. They are not equal to properties.
  4. Indexers allow the object to be indexed.
  5. Setting accessor will assign get and retrieve value.
  6. The value keyword is used when you set the value.
  7. Indexers are referred to as smart arrays or parameterized properties. though the latter might be misleading.
  8. Indexers cannot be static members as they are instance members of the class.

Overloading Indexers

The idea of overloading indexers is to imbue them with multiple arguments that allow us to support different datatypes. You can have different types of indexes—it's not mandatory to always use int. Multiple types allow you to build in flexibility and further increase the fault tolerance and robustness of the class and application. In order to achieve this, we need to declare it with multiple parameters, and each parameter should have different data types. The technique for the overload is very similar to the method for overloading. The very act of overloading is a C# feature intended to support one of the three pillars of object-oriented programming, or polymorphism.

Let's take a practical example.

      using System;

namespace ndexers
{   
    class Program
    {
        class Guides
        {            
            private string[] _guideNames = new string[10];
            
            public string this[int index]
            {
                get { return _guideNames[index]; }
                set { _guideNames[index] = value; }
            }
            
            public string this[float id]
            {
                get { return _guideNames[1]; }
                set { _guideNames[1] = value; }
            }

            public string this[double id]
            {
                get { return "This is read only"; }
                set { }
            }
        }
        static void Main(string[] args)
        {
            Guides writtenGuides = new Guides();
            double k = 10.0;
            writtenGuides[0] = "Written ";
            writtenGuides[1.0f] = "Guides";
            Console.WriteLine(writtenGuides[k]);
            Console.WriteLine(writtenGuides[0]);
            Console.WriteLine(writtenGuides[0] + writtenGuides[1.0f]);
            Console.ReadKey();
            }
    }
}
    

The output from the app is the following.

      This is read only
Written
Written Guides
    

Let's look at what's happening under the hood. In our class Guides, we have three indexers. One is working with argument type int, another is with argument type float, and the third, which is read-only, is working with the double argument type. Read-only accessors are used when you want to prevent modification to an indexer of a specific type. Basically, the setter is not defined, and this is how you make it read-only. The indexer reacts to different types of indexes appropriately, and this is why we see the int argument returns Written, the float argument returns the concatenation of the int and float-based indexes, and the double notifies us that it's read only.

Finally, let's take a look at multi-dimensional indexers.

      using System;

namespace ndexers
{   
    class Program
    {
        class MultiDimensional
        {            
            private string[,] _guideNames = new string[10,10];
            
            public string this[int x, int y]
            {
                get { return _guideNames[x,y]; }
                set { _guideNames[x,y] = value; }
            }            
        }
        static void Main(string[] args)
        {
            MultiDimensional theMatrix = new MultiDimensional();
            theMatrix[0,0] = "Daniel";
            theMatrix[0, 1] = "Florian";
            for(int i = 0; i < 10; i++)
            {
                for(int j = 0; j < 10; j++)
                {
                    if( theMatrix[i,j] == null) { Console.Write("N.A. "); }
                    else{ Console.Write($"{theMatrix[i, j]} "); }                    
                }
                Console.WriteLine();
            }
            Console.ReadKey();
            }
    }
}
    

The output is as follows.

      Daniel Florian N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A.
N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A.
N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A.
N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A.
N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A.
N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A.
N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A.
N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A.
N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A.
N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A. N.A.
    

Here we have the MultiDimensional class, which implements a 10-by-10 matrix that allows you to store strings based on the indexes you specify. There is nothing special about this. It works as other multidimensional arrays would, and the bonus is the class context, which allows you to add extra functionality. We instantiate our class, add some items, then iterate over with nested for loops.

Conclusion

All in all, indexers and their overloading allow us to extend class functionality. This guide has shown you three distinct applications: the simple, single type-based indexers; how to overload indexers; and multi-dimensional indexers. The latter also supports overriding so you can build in extra functionality. I hope this has been informative for you and you found what you were looking for. If you liked this guide, make sure you give it a thumbs up.

Dániel Szabó

Dániel Szabó

Written content author.

More about this author