Named Indexers

published: Fri, 24-Sep-2004   |   updated: Sun, 23-Jul-2006
Swan

So there was a posting on borland.public.delphi.non-technical raving about Delphi's array properties and bemoaning that C# didn't have them. The post seemed to be a standard "Delphi is better than C#" argument, but it had a couple of inaccuracies, which will make for an interesting article. Here's a quotation from the middle of the post.

[Delphi's] Array property is really handy for writing/wiring components and wrapping classes and data.

In C#, you could only have indexer which one for one class. Now when I port some Delphi code or Delphi 8 codes to C#, I have to change the interfaces to methods.

A quick recap about indexers is in order before we begin. An indexer is a way of viewing an instance of a class as if it were an array. In other words we can take an object of the class and pretend that it can be indexed using the familiar array syntax:

MyClass myObj = new MyClass();
...
someValue = myObj[42];

It is through indexers that we can write classes like ArrayList or Hashtable so that we can use them in a familiar array-like way. Unlike normal arrays, which are only indexed using an integer value, we can create an indexer that is indexed by other types, like a string. Here's the canonical Hashtable example:

someValue = myHashtable["Some key"];

So, like Delphi's array properties, indexers are fun and cool, and enable us to extend the syntax of arrays to objects that aren't exactly arrays.

In syntax, defining an indexer is bit like defining a property (there's getter and a setter clauses) but it doesn't have a name. It's also a bit like defining a method (there are parameters) but again there's no name and it's used with a different calling convention. It's best to view them as a facade on an object in order to use an object like an array.

The first point to bring up about the quotation above is the assertion that in C# you can only have one indexer per class (English is not the writer's native tongue, but I think that's what he means). Not so, of course. You can have as many as you want, so long as the signature for each of them is different. (The signature of an indexer is the index part, the bit between the square brackets. The return type, if any, doesn't play any part in the signature.)

Here's a rather short example where there are two indexers: one for an integer index, the other for a string index. The class is the start of a kind of "smart date" class: the integer indexer passes back the components (day, month, year) of the date, and the string indexer passes back the date formatted with the string index. A fun example of an object that isn't an array being used like an array.

using System;

namespace JMBucknall.SmartDate {
   public class SmartDate {
      private DateTime date;

      public SmartDate(DateTime date) {
         this.date = date;
      }

      public int this[int partNumber] {
         get {
            switch (partNumber) {
               case 0:
            		return date.Day;
               case 1:
                  return date.Month;
               case 2:
                  return date.Year;
               default:
                  throw new Exception("The part number must be 0, 1, or 2");
            }
         }
      }

      public string this[string format] {
         get {
            return date.ToString(format);
         }
      }
   }
}

Here's the test suite so that you can see how to call the two indexers and what they return.

using System;
using NUnit.Framework;
using JMBucknall.SmartDate;

namespace TestSmartDate {

   [TestFixture]
   public class SmartDateTester {

      [Test] public void TestIntIndexer() {
         SmartDate date = new SmartDate(new DateTime(2004, 9, 24));
         Assertion.AssertEquals(
            "SmartDate int indexer: The day part is wrong [0]", 
            24, date[0]);
         Assertion.AssertEquals(
            "SmartDate int indexer: The month part is wrong [1]", 
            9, date[1]);
         Assertion.AssertEquals(
            "SmartDate int indexer: The day part is wrong [2]", 
            2004, date[2]);
      }

      [Test] public void TestStringIndexer() {
         SmartDate date = new SmartDate(new DateTime(2004, 9, 24));
         Assertion.AssertEquals(
            "SmartDate string indexer: The dd/MM/yyyy formatter is wrong", 
            "24/09/2004", date["dd/MM/yyyy"]);
         Assertion.AssertEquals(
            "SmartDate string indexer: The dd-MMM-yyyy formatter is wrong [0]", 
            "24-Sep-2004", date["dd-MMM-yyyy"]);
      }
   }
}

Note that the calling of instance methods of this class is slightly easier than with Delphi. Although you can define many array properties with Delphi only one may be marked as default, meaning that with only one of these array properties can the name be omitted by the caller.

Now, the problem becomes how can we have two indexers in C# with the same index signature? Since an indexer has no name, the only way to identify an indexer is by its signature (again, the return type plays no part in the signature). That would seem to preclude having two indexers with the same signature, but we can get round this by naming the indexers. Since named indexers are not in the C# language, we shall have to fake them.

The code we would like to write to call a named indexer is of course something like the following:

SomeStringValue = MySmartDate.Format["{0:d}"];
SomeIntValue = MySmartDate.Part["d"];

Here I've promoted my original string indexer to a named indexer (Format) so that I could also have another named string indexer called Part that returns the various parts of the date (for a start we could have "d", "m", "y", and "dow" for day. month, year, and day of the week).

Let's see if we can discover this by working in reverse. From the way they're called, we can ascertain that the members are properties (they can't be methods since there's no parentheses in sight). Also the two members Part and Format must either be standard arrays or must be objects that themselves have normal unnamed indexers (for example, an ArrayList). Although the first option is viable, it seems a little like overkill. The second option gives us the best bet, although using an ArrayList would be just as much overkill as the first option. But, hey, we know how to write classes that aren't really array-like to contain an indexer.

Here's the SmartDate class rewritten to expose the two named indexers. I've made use of nested classes to keep the name pollution down a bit.

using System;

namespace JMBucknall.SmartDate {
   public class SmartDate {
      private DateTime date;

      public SmartDate(DateTime date) {
         this.date = date;
      }

      public PartHelper Part {
         get { return new PartHelper(this); }
      }

      public FormatHelper Format {
         get { return new FormatHelper(this); }
      }

      public class PartHelper {
         private SmartDate parent;

         public PartHelper(SmartDate parent) {
            this.parent = parent;
         }

         public int this[string partKind] {
            get {
               switch (partKind) {
                  case "d":
                     return parent.date.Day;
                  case "m":
                     return parent.date.Month;
                  case "y":
                     return parent.date.Year;
                  case "dow":
                     return (int) parent.date.DayOfWeek;
                  default:
                     throw new Exception("The part kind must be d, m, y or dow");
               }
            }
         }
      }

      public class FormatHelper {
         private SmartDate parent;

         public FormatHelper(SmartDate parent) {
            this.parent = parent;
         }

         public string this[string format] {
            get {
               return parent.date.ToString(format);
            }
         }
      }
   }
}

And here's the test code, showing how to call the named indexers.

using System;
using NUnit.Framework;
using JMBucknall.SmartDate;

namespace TestSmartDate {

   [TestFixture]
   public class SmartDateTester {

      [Test] public void TestPartIndexer() {
         SmartDate date = new SmartDate(new DateTime(2004, 9, 24));
         Assertion.AssertEquals(
            "SmartDate Part indexer: The day part is wrong", 
            24, date.Part["d"]);
         Assertion.AssertEquals(
            "SmartDate Part indexer: The month part is wrong", 
            9, date.Part["m"]);
         Assertion.AssertEquals(
            "SmartDate Part indexer: The day part is wrong", 
            2004, date.Part["y"]);
         Assertion.AssertEquals(
            "SmartDate Part indexer: The dayofweek part is wrong", 
            5, date.Part["dow"]);
      }
      
      [Test] public void TestFormatIndexer() {
         SmartDate date = new SmartDate(new DateTime(2004, 9, 24));
         Assertion.AssertEquals(
            "SmartDate Format indexer: The dd/MM/yyyy formatter is wrong", 
            "24/09/2004", date.Format["dd/MM/yyyy"]);
         Assertion.AssertEquals(
            "SmartDate Format indexer: The dd-MMM-yyyy formatter is wrong [0]", 
            "24-Sep-2004", date.Format["dd-MMM-yyyy"]);
      }

   }
}

As you can see, the named indexers are nothing more than standard properties exposing objects that have indexers.

Although this solution to defining named indexers in C# is perhaps not as elegant as Delphi's array properties, their use is unquestionably as easy and intuitive as Delphi's, making any code conversion simpler.

(Many thanks to Jay Bazuzi on the Microsoft C# IDE team for showing the way.)