5

考虑以下代码:

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总是调用期望 an 的重载(即使将 0 转换为 int 时也是如此),并且测试失败。

  1. 有人可以向我解释这种行为吗?
  2. 并给出一个解决方法来维护两个索引器:一个按索引,一个用于枚举?

编辑:一种解决方法似乎是将集合转换为集合,请参阅此答案

所以:

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

解释

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

  1. 0 值始终可以转换为任何枚举。
  2. 运行时总是在挖掘继承之前检查底层类,因此选择了枚举索引器。

有关更精确(和更好的公式化)的答案,请参阅以下链接:

4

3 回答 3

11

这里的各种答案已经解决了。总结并提供一些解释材料的链接:

首先,文字零可以转换为任何枚举类型。这样做的原因是因为我们希望您能够将任何“标志”枚举初始化为其零值,即使没有可用的零枚举值。(如果我们不得不重新做一遍,我们可能不会实现这个功能;相反,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 和一个采用 enum - 在传递文字零时都是适用的候选者。那么问题是哪个是更好的候选人。这里的规则很简单:如果任何候选对象适用于派生类,那么它自动优于基类中的任何候选对象。因此,您的枚举索引器获胜。

这个有点违反直觉的规则的原因是双重的。首先,编写派生类的人比编写基类的人拥有更多信息似乎是有道理的。毕竟,他们专门化了基类,因此在给定选择时,您希望调用可能的最专门化的实现似乎是合理的,即使它不是完全匹配的。

第二个原因是这种选择缓解了脆弱的基类问题。如果您将索引器添加到恰好比派生类上的匹配更好的基类,则派生类的用户会意外地发现,用于选择派生类的代码突然开始选择基类。

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

有关此问题的更多讨论。

正如 James 正确指出的那样,如果您在类上创建一个采用 int 的新索引器,那么重载解决问题就会变得更好:从零到枚举的转换,或从零到 int 的转换。由于两个索引器的类型相同,并且后者是精确的,因此它获胜。

于 2011-09-19T21:23:03.360 回答
5

似乎因为enumis int-compatible,它更喜欢使用从enumto的隐式转换int并选择采用您的类中定义的枚举的索引器。

更新const int:真正的原因是它更喜欢从of0enum超类索引器的隐式转换,int因为两个转换是相等的,所以选择前一个转换,因为它在更派生的类型内部:MyCollection.)

我不确定为什么会这样,因为显然有一个公共索引器int从那里提出了一个论点Collection<T>——如果 Eric Lippert 正在看这个,这对 Eric Lippert 来说是一个很好的问题,因为他会有一个非常明确的答案。

不过,我确实验证过,如果您在新类中重新定义 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 隐式枚举转换 隐式枚举转换允许将十进制整数文字 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,其中部分说明:

如果派生类中的任何方法适用,则基类中的方法和方法不是候选对象

这让我相信,由于子类索引器有效,它甚至不检查基类。

于 2011-09-19T17:48:06.823 回答
2

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

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

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

在 C# 的早期版本中,只有文字0会被隐式转换为枚举类型。从 3.0 版开始,所有计算结果为的常量表达式都0可以隐式转换为枚举类型。这就是为什么 even(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"
于 2011-09-19T18:25:33.713 回答