使用枚举重载索引器:无法使用默认索引器

时间:2011-09-19 17:04:51

标签: c# enums overloading indexer

考虑以下代码:

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
    }
}

我很惊讶地看到以下结果:

1) Null ? True
2) Null ? False

我的第一个期望是因为我传递的是int,所以应该使用默认索引器,并且第一次调用应该已成功。

相反,似乎总是调用期望enum的重载(即使将0转换为int),并且测试失败。

  1. 有人可以向我解释这种行为吗?
  2. 并提供一个解决方法来维护两个索引器:一个是索引,另一个是枚举?
  3. 编辑:解决方法似乎是将集合转换为集合,请参阅this answer

    所以:

    1. 为什么编译器会选择最“复杂”的重载而不是最明显的重载(尽管它是一个继承的重载)?索引器是否被视为native int方法? (但没有警告您隐藏父索引器的事实)
    2. 解释

      使用此代码,我们面临两个问题:

      1. 0值始终可以转换为任何枚举。
      2. 运行时总是从挖掘继承之前检查底层类开始,因此选择了枚举索引器。
      3. 有关更精确(和更好的表达)的答案,请参阅以下链接:

3 个答案:

答案 0 :(得分:10)

这里的各种答案都证实了这一点。总结并提供解释性材料的一些链接:

首先,文字零可以转换为任何枚举类型。这是因为我们希望您能够将任何“flags”枚举初始化为零值,即使没有可用的枚举值也是如此。 (如果我们不得不重新做一遍,我们可能不会实现这个功能;相反,如果你想这样做,我们会说只使用default(MyEnum)表达式。)

实际上,常量,而不仅仅是文字常量零可以转换为任何枚举类型。这是为了向后兼容一个历史悠久的编译器错误,这个错误比修复更加昂贵。

有关详情,请参阅

http://blogs.msdn.com/b/ericlippert/archive/2006/03/28/the-root-of-all-evil-part-one.aspx

http://blogs.msdn.com/b/ericlippert/archive/2006/03/29/the-root-of-all-evil-part-two.aspx

然后确定你的两个索引器 - 一个采用int和一个采用枚举 - 在传递文字零时都是适用的候选者。那么问题是更好的候选人。这里的规则很简单:如果任何候选者适用于派生类,那么它自动优于基类中的任何候选者。因此,你的枚举索引器获胜。

这种有点违反直觉的规则的原因是双重的。首先,似乎有意义的是,编写派生类的人比编写基类的人有更多的信息。毕竟,他们专门研究基类,所以在给出选择时你想要调用最专业的实现似乎是合理的,即使它不是完全匹配的。

第二个原因是这个选择减轻了脆弱的基类问题。如果您将一个索引器添加到基类,它恰好是一个比派生类更好的匹配,那么派生类的用户会突然选择用于选择派生类的代码开始选择基类。

http://blogs.msdn.com/b/ericlippert/archive/2007/09/04/future-breaking-changes-part-three.aspx

有关此问题的更多讨论。

正如James正确指出的那样,如果你在类上创建一个带有int的新索引器,那么重载决策问题会变得更好:从零转换为枚举,或从零转换为int。由于两个索引器属于同一类型而后者是 exact ,因此它将获胜。

答案 1 :(得分:5)

似乎因为enumint - 兼容,所以它更喜欢使用从enumint的隐式转换,并选择采用枚举定义的索引器在你的班上

更新:真正的原因是它更喜欢从const int 0enum的隐式转换超过超级-class int索引器,因为两个转换都是相同的,所以选择前一个转换,因为它在更多派生类型中:MyCollection。)

我不确定为什么会这样做,当int显然有一个Collection<T>参数的公共索引器时 - 对于Eric Lippert来说这是一个很好的问题d有一个非常确定的答案。

但我确实验证了如果你在新类中重新定义int索引器,如下所示,它将起作用:

public class MyCollection : Collection<MyItem>
{
    public new MyItem this[int index]
    {
            // make sure we get Collection<T>'s indexer instead.
        get { return base[index]; }
    }
}

从规范看,文字0似乎总是隐式转换为enum

  

13.1.3隐式枚举转换隐式枚举   转换允许将decimal-integer-literal 0转换为   任何枚举类型。

因此,如果您将其称为

        int index = 0;
        var item = col[index];

它会起作用,因为你强迫它选择int索引器,或者你使用的是非零文字:

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

1无法隐式转换为enum

以来可行

它仍然很奇怪,我允许你考虑来自Collection<T>的索引器应该同样可见。但我会说它看起来像你的子类中的enum索引器,并且知道0可以隐式地转换为int并且满足它并且不会上升到类 - 等级链。

这似乎得到规范中7.4.2 Overload Resolution部分的支持,其中部分说明:

  如果a中的任何方法,

和基类中的方法不是候选者   派生类适用

这使我相信,由于子类索引器起作用,它甚至不检查基类。

答案 2 :(得分:2)

在C#中,内容0始终可以隐式转换为任何枚举类型。您已经重载了索引器,因此编译器会选择最具体的重载。请注意,这在编译期间发生。所以,如果你愿意写:

int x = 0;
var item = col[x];

现在编译器没有推断第二行x总是等于0,所以它会选择原始的this[int value]重载。 (编译器不是很聪明: - ))

在C#的早期版本中,只有文字0会隐式转换为枚举类型。 Since version 3.0,所有计算为0的常量表达式都可以隐式地转换为枚举类型。这就是为什么偶然(int)0被归为枚举。

更新:有关重载决策的额外信息

我一直认为重载解析只是看了方法签名,但它似乎也更喜欢派生类中的方法。例如,考虑以下代码:

public class Test
{
    public void Print(int number)
    {
        Console.WriteLine("Number: " + number);
    }

    public void Print(Options option)
    {
        Console.WriteLine("Option: " + option);
    }
}

public enum Options
{
    A = 0,
    B = 1
}

这将导致以下行为:

t.Print(0); // "0"
t.Print(1); // "1"
t.Print(Options.A); // "A"
t.Print(Options.B); // "B"

但是,如果您创建基类并将Print(int)重载移动到基类,则Print(Options)重载将具有更高的首选项:

public class TestBase
{
    public void Print(int number)
    {
        Console.WriteLine("Number: " + number);
    }
}

public class Test : TestBase
{
    public void Print(Options option)
    {
        Console.WriteLine("Option: " + option);
    }
}

现在行为发生了变化:

t.Print(0); // "A"
t.Print(1); // "1"
t.Print(Options.A); // "A"
t.Print(Options.B); // "B"