Search code examples
c#enumsoverloadingindexer

Overloaded indexer with enum : impossible to use default indexer


Considering the following code:

namespace MyApp
{
    using System;
    using System.Collections.ObjectModel;

    class Program
    {
        static void Main(string[] args)
        {
            var col = new MyCollection();
            col.Add(new MyItem { Enum = MyEnum.Second });
            col.Add(new MyItem { Enum = MyEnum.First });

            var item = col[0];
            Console.WriteLine("1) Null ? {0}", item == null);

            item = col[MyEnum.Second];
            Console.WriteLine("2) Null ? {0}", item == null);

            Console.ReadKey();
        }
    }

    class MyItem { public MyEnum Enum { get; set; } }

    class MyCollection : Collection<MyItem>
    {
        public MyItem this[MyEnum val]
        {
            get
            {
                foreach (var item in this) { if (item.Enum == val) return item; }
                return null;
            }
        }
    }

    enum MyEnum
    {
        Default = 0,
        First,
        Second
    }
}

I was surprised to see the following result:

1) Null ? True
2) Null ? False

My first expectation was that because I was passing an int, the default indexer should be used, and the first call should have succeeded.

Instead, it seems that the overload expecting an enum is always called (even when casting 0 as int), and the test fails.

  1. Can someone explain this behavior to me?
  2. And give a workaround to maintain two indexers: one by index, and one for the enum?

EDIT : A workaround seems to be casting the collection as Collection, see this answer.

So:

  1. Why does the compiler choose the most "complex" overload instead of the most obvious one (despite the fact it's an inherited one)? Is the indexer considered a native int method? (but without a warning on the fact that you hide the parent indexer)

Explanation

With this code we are facing two problems:

  1. The 0 value is always convertible to any enum.
  2. The runtime always starts by checking the bottom class before digging in inheritance, so the enum indexer is chosen.

For more precise (and better formulated) answers, see the following links:


Solution

  • The various answers here have sussed it out. To sum up and provide some links to explanatory material:

    First, the literal zero is convertible to any enum type. The reason for this is because we want you to be able to initialize any "flags" enum to its zero value even if there is no zero enum value available. (If we had to do it all over again we'd probably not implement this feature; rather, we'd say to just use the default(MyEnum) expression if you want to do that.)

    In fact, the constant, not just the literal constant zero is convertible to any enum type. This is for backwards compatibility with a historic compiler bug that is more expensive to fix than to enshrine.

    For more details, see

    https://learn.microsoft.com/en-us/archive/blogs/ericlippert/the-root-of-all-evil-part-one

    https://learn.microsoft.com/en-us/archive/blogs/ericlippert/the-root-of-all-evil-part-two

    That then establishes that your two indexers -- one which takes an int and one which takes an enum -- are both applicable candidates when passed the literal zero. The question then is which is the better candidate. The rule here is simple: if any candidate is applicable in a derived class then it is automatically better than any candidate in a base class. Therefore your enum indexer wins.

    The reason for this somewhat counter-intuitive rule is twofold. First, it seems to make sense that the person who wrote the derived class has more information than the person who wrote the base class. They specialized the base class, after all, so it seems reasonable that you'd want to call the most specialized implementation possible when given a choice, even if it is not an exact match.

    The second reason is that this choice mitigates the brittle base class problem. If you added an indexer to a base class that happened to be a better match than one on a derived class, it would be unexpected to users of the derived class that code that used to choose the derived class suddenly starts choosing the base class.

    See

    https://learn.microsoft.com/en-us/archive/blogs/ericlippert/future-breaking-changes-part-three

    for more discussion of this issue.

    As James correctly points out, if you make a new indexer on your class that takes an int then the overload resolution question becomes which is better: conversion from zero to enum, or conversion from zero to int. Since both indexers are on the same type and the latter is exact, it wins.