代码之家  ›  专栏  ›  技术社区  ›  Impworks

C#方法凌驾解析怪诞

  •  24
  • Impworks  · 技术社区  · 6 年前

    using System;
    
    class Base
    {
        public virtual void Foo(int x)
        {
            Console.WriteLine("Base.Foo(int)");
        }
    }
    
    class Derived : Base
    {
        public override void Foo(int x)
        {
            Console.WriteLine("Derived.Foo(int)");
        }
    
        public void Foo(object o)
        {
            Console.WriteLine("Derived.Foo(object)");
        }
    }
    
    public class Program
    {
        public static void Main()
        {
            Derived d = new Derived();
            int i = 10;
            d.Foo(i);
        }
    }
    

    令人惊讶的结果是:

    Derived.Foo(object)
    

    Foo(int x) 方法,因为它更具体。但是,C编译器选择非继承的 Foo(object o)

    这种行为的原因是什么?

    2 回复  |  直到 6 年前
        1
  •  29
  •   TheGeneral    6 年前

    这是规定,你可能不喜欢。。。

    引用自 Eric Lippert

    如果更派生类上的任何方法是适用的候选方法,则 在派生较少的类上自动优于任何方法,甚至

    原因是因为该方法(这是一个更好的签名匹配)可能已添加到更高版本中,从而引入了一个“ brittle base class “失败


    注意 :这是C规范中相当复杂/深入的一部分,它到处都是。但是,您遇到的问题的主要部分如下所示

    更新

    我引用的是 方法调用的运行时处理 ,应该是。

    7.6.5.1方法调用

    候选方法集被简化为只包含来自 在其中声明方法F的类型,所有方法都在一个基中声明 . 此外,如果C是类类型 除了object,接口类型中声明的所有方法都是 从集合中删除(后一条规则只有在 方法组是对类型参数进行成员查找的结果 有一个有效的基类而不是object和一个非空的

    请看埃里克的帖子答案 https://stackoverflow.com/a/52670391/1612975 关于这里发生了什么和规格的适当部分的完整细节

    C级# 语言规范 版本5.0

    7.5.5函数成员调用

    ...

    函数成员调用的运行时处理包括 以下步骤,其中M是函数成员,如果M是 实例成员,E是实例表达式:

    ...

    如果M是引用类型中声明的实例函数成员:

    • 如果E的类型是值类型,则装箱转换(§4.3.1)将E转换为object类型,E被认为是 按以下步骤键入object。在这种情况下,M只能是a
    • 检查E的值是否有效。如果E的值为null,则会引发System.NullReferenceException,并且不会执行进一步的步骤
    • 要调用的函数成员实现已确定:
      • E引用的实例的类型 通过应用接口映射规则确定(§13.4.4)至 确定运行时类型提供的M的实现 E引用的实例。
      • 否则,如果M是虚拟函数成员,则要调用的函数成员是运行时类型提供的M的实现 由E引用。

      public interface ITest
      {
         void Foo(int x);
      }
    

    Which can be shown here

    在接口方面,当考虑到实现重载行为是为了防止基类脆弱时,这是有意义的


    额外资源

    Eric Lippert, Closer is better

    实际上,判断一个潜在过载的基本规则 对于一个给定的调用站点,要比另一个更好:总是越接近越好

    • 在派生类中首先声明的方法比在基类中首先声明的方法更接近。
    • 嵌套类中的方法比包含类中的方法更接近。
    • 任何接收类型的方法都比任何扩展方法更接近。
    • 在嵌套命名空间的类中找到的扩展方法比在外部命名空间的类中找到的扩展方法更接近。
    • 由using指令提及。
    • 在using指令中提到的命名空间中的类中找到的扩展方法(该指令位于嵌套命名空间中)更接近 而不是在中提到的命名空间中的类中找到的扩展方法 一个using指令,其中该指令位于外部命名空间中。
        2
  •  15
  •   Eric Lippert    6 年前

    正当理由 为什么规格是好的。

    假设我们有基类B和派生类D。B有一种方法M,就是长颈鹿。现在,记住, . 换言之:D的作者必须知道 更多 比B的作者,因为 ,和 编写D是为了将B扩展到B尚未处理的场景 . 因此,我们应该相信D的作者正在做一个 实施工作 全部的 D的功能比B的作者强。

    他们说,如果D的作者制造了一个超负荷的M来带走一只动物 . 当给D.M(长颈鹿)打电话叫D.M(动物),而不是给B.M(长颈鹿)打电话时,我们应该期望过载解决。

    • 打给D.M(长颈鹿)的电话应该打给B.M(长颈鹿),因为长颈鹿比动物更特异

    两个理由都是关于 特异性 ,那么哪种理由更好呢? 我们不会用任何方法对付动物 ! 我们正在调用D上的方法,所以 专一性应该是赢家。特殊性 接受者 比任何一个参数的特异性都重要得多。 . 重要的是确保我们选择最具体的 接受者 因为 这个方法是后来由一个更了解D要处理的场景的人编写的

    弗斯特 ,D的作者应该知道D.M(动物) ,并且必须按照 正确的事情

    实施细则 当然是D,不是 公共区域的一部分 更改是否重写方法 变化 选择哪种方法 . 想象一下,如果在一个版本中调用某个基类上的方法,然后在下一个版本中,基类的作者对是否重写某个方法做了一个小的更改;你不会期望超负荷解决 在派生类中 改变。C#经过精心设计,可防止此类故障。

    推荐文章