为什么在具有相同名称的更具特异性的覆盖方法上采用较少的特定过载?

Rad*_*icz 4 c# inheritance overriding

我有以下非常简单的代码:

public class Base
{
    public virtual void Foo(int x)
    {
        Console.WriteLine("Base.Foo(int)");
    }
}
public class Derived : Base
{
    public override void Foo(int x)
    {
        Console.WriteLine("Derived.Foo(int)");
    }
    public void Foo(object o)
    {
        Console.WriteLine("Derived.Foo(object)");
    }
}
Run Code Online (Sandbox Code Playgroud)

然后在程序中写道:

Derived d = new Derived();
int i = 1;
d.Foo(i); //prints Derived.Foo(object)
Run Code Online (Sandbox Code Playgroud)

我不明白,也无法在网上找到为什么调用Derived.Foo(对象)?如何确保调用Derived.Foo(int)

Eri*_*ert 18

我不明白,也无法在网上找到为什么调用Derived.Foo(对象)?

规则是更具体的比一般更好.但是在这种情况下我们有一个冲突:int比对象更具体,所以Base.Foo(int)应该获胜,但是声明在派生中比声明在底更具体,所以Derived.Foo(object)应该赢.

考虑它的另一种方式是:"这个"在逻辑上是一个无形的论证,所以我们可以把签名Foo(int)看作是一个看不见的Base,Foo(object)就像一个看不见的Derived.重载决策应该更喜欢int to object,但也应该更喜欢Derived to Base,我们有一个矛盾.

C#必须解决这个冲突.

C#中的规则是派生类声明的适用方法总是优于基类声明的任何方法.此外:重写的虚方法被认为是在原始类中声明它的方法,而不是在覆盖它的类中.

虽然很多人一开始觉得这种违反直觉,但这是一个经过仔细考虑的决定,旨在(1)确保派生类作者 - 他们拥有关于派生类的正确行为的更多信息,而不是基类作者! - 可以控制派生类的行为,以及(2)减轻脆弱的基类失败.

它减轻了几个脆弱的基类故障; 特别是,其中一个是"程序根据类层次结构中覆盖发生位置的细微细节来改变其行为".你永远不希望在一个情况下你的派生类代码符,因为别人的三个基本类中深层次的感动过载,从一个地方到另一个地方.这些细节应该是基类的不可见的实现细节,因此C#通过其规则来减轻此失败,即重载解析仅考虑最初声明虚方法的位置,并且从不重写它的位置.

我在重载决议中有很多关于这个和其他微妙决定的文章.这里有几个有用的链接; 考虑浏览我的WordPress和MSDN博客,了解更多相关主题的文章.

https://blogs.msdn.microsoft.com/ericlippert/2007/09/04/future-breaking-changes-part-three/

https://ericlippert.com/2013/12/23/closer-is-better/

如何确保调用Derived.Foo(int)?

从坏主意到好主意,有几种可能性:

第一个想法:制作一个非虚方法.(坏!)

public class Derived : Base
{
  public new void Foo(int x)
  { 
    Console.WriteLine("Derived.Foo(int)");
  }
  public void Foo(object o)
  {
    Console.WriteLine("Derived.Foo(object)");
  }
}
Run Code Online (Sandbox Code Playgroud)

现在Derived.Foo(int)被调用,因为base和derived之间不再存在冲突.派生类中有两种适用的方法,而采用int的方法明显更好.

请注意,我们不再在此处进行虚拟重载,因此对Foovia 的调用Base将调用基类版本.这看起来很糟糕!这违反了基类作者给您的设计,他可能依赖于您进行正确的虚拟覆盖.

第二个想法:让调用者解决您创建的问题.(也不好!)

Derived d = new Derived();
int i = 1;
((Base)d).Foo(i);
Run Code Online (Sandbox Code Playgroud)

由于Foo是虚拟的,因此最终将调用派生方法.但这要求来电者知道你实施了一点"陷阱".这是一个陷阱; 不要让你的用户陷入困境.

像我这样的老程序微软程序员称这些API陷阱为"糖果机接口".请参阅https://blogs.msdn.microsoft.com/ericlippert/2008/09/08/high-maintenance/

界面自然会让你以错误的方式调用它.不要将其强加于您的用户.

第三个想法:你实现了这个烂摊子; 你修好了 (好)

您询问了如何确保调用Foo(int).但你不能!所以你必须让Foo(对象)做正确的事.

public void Foo(object o)
{
    if (o is int)
        ((Base)this).Foo((int)o));
    else
        Console.WriteLine("Derived.Foo(object)");
}
Run Code Online (Sandbox Code Playgroud)

当您编写Foo(对象)时,您说Derived.Foo(对象)知道如何在没有任何超载解析帮助的情况下完全正确地处理任何对象.所以实现那些语义; 这就是你在写这个签名时注册的内容.

第四个想法:如果你这样做会伤害,不要这样做.(最好)

通过不首先创建它来解决问题.简单地说,永远不要在派生类中创建更通用的方法; 它令人困惑,几乎总是错的.将更通用的方法移动到基类中.或者完全找到另一种设计.

跟进:

一位评论者询问我们有什么

public static void Bar(Action<object> a) { } 
public static void Bar(Action<int> a) { } 
Run Code Online (Sandbox Code Playgroud)

并致电:

    Bar(d.Foo);
Run Code Online (Sandbox Code Playgroud)

现在发生了什么?

这是一个模棱两可的错误.让我们看看为什么.这很微妙!


更新:下面的分析基于我对C#重载决策的理解,因为我在2012年11月离开微软时离开了它.我从Roslyn的消息来源看到,这里描述的精确场景激发了重载决策规则的微妙变化,记录在这里:https://github.com/dotnet/roslyn/issues/6560

因此,下面的分析应该被认为适用于C#5和6,但不适用于C#7.我不知道C#7的准确规则是什么.请参阅Roslyn源代码和github问题以获取详细信息!

但等等,情况变得更糟.在相关的差异中,Aleksey在评论中指出,这种变化是由向后兼容问题推动的,这意味着这种变化很可能是为了使C#与现有规范违规错误保持一致.

因此,下面的分析甚至可能对C#5无效,因为它假定编译器实现了规范.

显然,这有点乱.在尝试推断C#中的重载解析边缘情况时要小心.


首先,可d.Foo兑换成Action<object>?是.规则是:如果我们进行了重载决策object x = default(object); d.Foo(x),那么重载决策会成功吗?是的,它会,它会选择Derived.Foo(object).因此Bar(Action<object>)适用.

二,可d.Foo兑换成Action<int>?是.再次,如果我们做过载决议int x = default(int); d.Foo(x);就会重载决议成功吗?是的,它会Derived.Foo(object)再次产生.因此Bar(Action<int>)适用.

现在在这里坚持一下.这将是非法使用Derived.Foo(object)的类型的委托Action<int>所以为什么存在一个转换

这是C#设计中最微妙和最有争议的一点; Mads和我在C#3的设计过程中对此感到痛苦.有些声明存在但仍然非法使用,这就是其中之一.正如规范哦 - 如此明确地说:

请注意,从E到D的隐式转换的存在并不能保证转换的编译时应用程序在没有错误的情况下成功.

哇,C#.只是......哇

所以.我们现在有两个适用的重载,必须选择两者中最独特的一个.有一个独特的最佳?号码Action<int>不可转换为或不转换Action<object>,因此两者都不具体.因此这是一个错误.

你可以提出这个论点 - 并且相信我,很多人 - 我们应该说,如果我们没有Bar(Action<object>),我们会得到一个错误,那么Bar(Action<int>)应该是不适用的,因此Bar(Action<object>)胜利.虽然我对这一论点表示同情,但请记住我们在这里推论的内容:我们正在推理一个不应该首先出现的疯狂局面.过载分辨率需要在通用代码中给出合理的答案; 它有时会在疯狂的代码中给出疯狂的答案是不幸的,但对设计团队来说并不是一个重要的优先事项.此外,C#的一个好的设计原则是"当编译器不能轻易弄清楚重载决策的结果是什么时,回溯找到任何可行的解决方案可能是一个坏主意".简而言之,"如果你无法弄清楚,猜测"是JavaScript和Visual Basic的设计原则,而不是C#.在C#中,设计原则是"如果它不明确,请提醒开发人员他们有设计问题".

如果不是方法组而是传递一个等效的lambda,那么这里还有一些额外的细微之处,但除非你有一个特定的问题,否则我不会进入这些细节.

  • 有时候我希望我能不止一次地投票.在这种情况下,一个用于清晰和全面解释的投票,一个用于"你实现了这个混乱;你修复它.",两个用于"如果你这样做会伤害,不要那样做".确实,黄金法则. (7认同)