【问题标题】:Overloaded indexer with enum : impossible to use default indexer枚举重载索引器:无法使用默认索引器
【发布时间】:2011-11-20 10:28:34
【问题描述】:

考虑以下代码:

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. 并提供一种解决方法来维护两个索引器:一个按索引,一个用于枚举?

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

所以:

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

说明

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

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

如需更精确(和更好表述)的答案,请参阅以下链接:

【问题讨论】:

    标签: c# enums overloading indexer


    【解决方案1】:

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

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

    我不确定为什么会这样,因为显然有一个公共索引器带有来自 Collection&lt;T&gt;int 论点——如果 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&lt;T&gt; 的索引器应该同样可见。但我想说它看起来像是在您的子类中看到了 enum 索引器,并且知道 0 可以隐式转换为 int 并满足它并且不会上升到类层次结构链。

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

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

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

    【讨论】:

    • 你会想要public new MyItem this,因为你隐藏了基类的实现。
    • @Jim:很好,Resharper 给了我曲线,但我没有检查出来。无论如何,这是一个隐含的new,但总是很明确。
    • 我同意你的回答,但正如你所说,根本问题是“为什么编译器选择这个签名而不是最明显的一个”?我在我的问题中添加了一个新观点。
    • @Styx31:这是个好问题。如果我不在工作,我会深入研究 C# 规范以找到一些提示......我会看看我能挖掘出什么。
    • @Styx31:更新了规范中的一些有趣发现。
    【解决方案2】:

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

    首先,文字零可以转换为任何枚举类型。这样做的原因是因为我们希望您能够将任何“标志”枚举初始化为其零值,即使没有可用的零枚举值。 (如果我们必须重新做一遍,我们可能不会实现这个功能;相反,如果你想这样做,我们会说只使用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-breaking-changes-part-three.aspx

    更多关于这个问题的讨论。

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

    【讨论】:

    • 感谢所有链接和解释,我实际上面临两个问题:“0”字面行为与继承和方法解析相结合。
    【解决方案3】:

    在 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"
    

    【讨论】:

      猜你喜欢
      • 2016-06-20
      • 1970-01-01
      • 2012-06-13
      • 2014-10-26
      • 2020-09-27
      • 1970-01-01
      • 2019-07-13
      • 2022-08-15
      • 2020-04-03
      相关资源
      最近更新 更多