【问题标题】:What function will be called?会调用什么函数?
【发布时间】:2019-02-11 18:01:58
【问题描述】:

之前我问了一个没有得到完全回答的问题,因此我决定重新提出我的问题以了解发生了什么:

这是我的类层次结构:

interface I
{
    void f();
}

class A : I
{
    // non virtual method
    public void f()
    {
        Debug.Log("---->> A ");
    }
}

class B : A
{
    // non overriding but hiding class A method
    public void f()
    {
        Debug.Log("---->> B ");
    }
}

class C : I
{
    // non virtual method
    public void f()
    {
        Debug.Log("---->> C ");
    }
}

这里是执行代码:

Random rnd = new Random();
var randomI = rnd.Next(0, 2);

I i = null;
if (randomI == 0)
{
     i = new B(); 
}
else
{
    i = new C();
}
i.f(); 

就像现在一样,它要么输出A,要么输出C。它不会输出B。

问题来了:您能否解释一下如何通过这些步骤来决定调用哪个函数?

  1. 何时决定调用哪个函数 - 运行时或编译时?
  2. 如何决定调用什么函数的机制是什么?

【问题讨论】:

  • @Neil 为什么你认为这段代码在某处被使用,它可以用于教育目的?
  • 我遇到过使用这些边缘案例的(专业)代码,当新开发人员看到它时,他们不明白为什么它有时会起作用,它总是涉及大量的谷歌搜索/SO 和误解.我想你会收到编译器警告。听他们的。
  • 这是在unity里面吗?
  • @Narek 如果要显示B,请将B 更改为class B : A, I
  • B 中隐藏方法会产生编译器警告是有原因的——这正是提醒您注意不会以明显方式调用方法的事实。

标签: c# interface


【解决方案1】:

在编译时它绑定一个对I接口的调用,然后在运行时它调用实现I.f()的继承链中的顶级方法。

所以,在你的代码中

A a = new A();
a.f();

B b = new B();
b.f();

将使编译器执行以下指令:

  • 创建类 A 的实例并分配给“a”
  • 获取分配给“a”的对象并调用位于继承链顶部并实现A.f()的方法。在这种情况下是 A.f() 本身
  • 创建一个 B 类的实例并赋值
  • 获取分配给“a”的对象并调用该方法,该方法位于继承链的顶部并实现B.f()。在这种情况下是 B.f() 本身

这导致“A”和“B”。

但是,当你这样做时:

I i;
i = new B();
i.f();

你让它编译以下指令:

  • 声明一个变量“i”
  • 创建一个新对象 B 并将其分配给“i”
  • 获取分配给“i”的对象并调用该方法,该方法位于继承链的顶部并实现I.f()。是 A.f(),因为类 B 没有实现接口 I

i.f() 行它不知道new B() 被分配给i,它可以从其他地方传递。它只知道有一些抽象对象实例实现了I,它需要调用它的f() 方法。

您可以将new 方法想象成具有不同名称的方法:

public class B : A
{
    // non overriding but hiding class A method
    public void anotherName()
    {
       Debug.Log("---->> B ");
    }
} 

A a = new A();  
a.f();
B b = new B();
b.anotherName();

I i = new B();
i.f(); // this will obviously call the `A.f()` because there is no such method in B

唯一的区别是你不能为继承的类实例调用隐藏方法。

【讨论】:

  • 编译时间如何?
  • @Fildor 这有点误导,我已经更新了答案
  • 位于继承链的顶端 听起来有点令人困惑。对我来说,您的意思是在最低子类中找到的那个 - 继承最后一个并实现 I 的那个,对吗?
  • 对于这个答案,我会从这里添加@ikkentim 的答案stackoverflow.com/questions/52203479/… 特别是最后一部分:接口实现在 IL 中被标记为虚拟,即使它在 C# 中没有被标记为虚拟。该方法在 IL 中也被标记为 final,如果该方法在 C# 中是虚拟的,则不会被标记为 final。 。这有助于 A LOT 理解调度机制。
【解决方案2】:

当决定调用什么函数时 - 运行时或编译 时间?

编译时,如果有人将B 转换为I 并在其上调用f,编译器会确定A.f 是要调用的方法。 在运行时,如果涉及B 的实例(相对于C 的实例),它会调用该方法。 换句话说,关键决定是在编译时做出的。

请注意,如果方法是 virtual,请参阅 @YeldarKurmangaliyev 的回答,了解它如何调用“继承链中的顶级方法”(但这不是这里的场景)。

如何决定调用什么函数的机制是什么?

specification的相关部分是13.4.5接口实现继承

类继承其基类提供的所有接口实现 类。在没有显式重新实现接口的情况下,派生的 类不能以任何方式改变它继承的接口映射 它的基类。

这就是为什么 class B : A 显示 Aclass B : A, I 显示 B。因为使用后者,您正在显式地重新实现接口。

规范中的示例(基本上是您的场景):

类继承其基类提供的所有接口实现 类。在没有显式重新实现接口的情况下,派生的 类不能以任何方式改变它继承的接口映射 它的基类。例如,在声明中

interface IControl
{
    void Paint();
}
class Control: IControl
{
    public void Paint() {...}
}
class TextBox: Control
{
    new public void Paint() {...}
}

TextBox 中的 Paint 方法隐藏了 Control 中的 Paint 方法,但它 不会改变 Control.Paint 到 IControl.Paint 的映射,并且 通过类实例和接口实例调用 Paint 将 有以下效果

Control c = new Control();
TextBox t = new TextBox();
IControl ic = c;
IControl it = t;
c.Paint();          // invokes Control.Paint();
t.Paint();          // invokes TextBox.Paint();
ic.Paint();         // invokes Control.Paint();
it.Paint();         // invokes Control.Paint();

规范还谈到了使用virtual(这是一种比明确指定B实现I更常见的解决方案):

然而,当一个接口方法被映射到一个虚拟方法上时 类,派生类可以覆盖虚拟 方法并更改接口的实现。

【讨论】:

    【解决方案3】:

    恕我直言,有趣的部分是为什么:((I)new B()).f() 打印

    ---->> A
    

    B 转换为I,将使用基类方法。如果您想打印 ---->> B 并保留 if/else 分支,则必须将 i 转换为 B ,这将显式调用 B 的实现:

    if (i is B)
       ((B)i).f();
    else
       i.f();
    

    当转换为 I 时,你的类声明就是这样:

    I -> A -> B
    |_ f() is implemented in subclasses, let's go one step down;
    
    I -> A -> B
         |_ f() is found, let's call A's f();
    

    如果你想强制调用B的实现,让B直接实现I:

    class B : A, I
    

    因此,当投射到 I 时,会发生这种情况:

    // Paths from I to B
    I -> A -> B 
    I -> B // Shorter path, let's go via this one.
    
    I -> B
    |_ f() is implemented in subclasses, let's go one step down;
    
    I -> B
         |_ f() is found, let's call B's f();
    

    当然,这是真正发生的事情的一个更简单的版本,但它有助于理解这个概念

    【讨论】:

    • class B : A, I 将是它打印B 的另一种方式。
    • 即使使用new,仍然不会调用该方法。
    【解决方案4】:

    我认为这特别容易混淆那些(像我一样)了解 C++ 中的虚拟与非虚拟方法以及 Java 中的接口,后来不得不弄清楚两者在 C# 中如何交互的人。

    在 Java 中通过接口引用进行方法分派(相对而言)很简单,因为 Java 中的所有方法都是虚拟的。所以当然实例运行时类型决定调用什么,就是这样...... 在 Java 中

    这确实意味着实例、方法名称和实现之间的某些更复杂的关系在 Java 中不可用,这可以被认为是“一件好事”或“一件坏事”,具体取决于您的观点。

    无论如何,这是 Java 和 C# 之间的最大区别之一,因为在 C# 中不仅方法不需要是虚拟的,而且默认情况下它们也不是。在不涉及实施细节的情况下,这会导致一个关键的见解:

    如果父类中的方法未标记为virtual,则您不能将子类中的对应(相同名称和签名)方法标记为override。另外,如果子类中的方法没有标记为override,那么它的行为就像new 无论它在基类中是否是虚拟的 em>。

    所以...如果一个类使用非virtual 方法实现接口,当您想通过接口引用调用该方法时,是否使用了类似虚拟的调度?这似乎是一种合理的方法。 (想想接口引用可能最终指向特定类的实例的所有方式,当编译使用接口引用的代码时,这可能是不可知的。虚拟调度在这里才有意义。)但这并不是唯一的方式.

    但是

    没关系。即使是这样,在 C# 中virtual 并不意味着子类实现必须覆盖基类方法;它仅仅意味着它可以如果宣布这样做。如果不显式声明基类方法virtual,您就无法使用声明自己覆盖该方法的方法来编译子类。


    更新 :当前评分最高的答案声称关键决策是在编译时做出的,虽然实现可以在具体示例中这样做,但我不相信它 - 仅仅因为这种方法不能一概而论。

    public void myMethod(I i) {
        i.f();
    }
    

    在编译上述方法时,编译器完全不知道它应该调用什么实际实现。并且在使用该行编译其他一些代码单元时

    myMethod(new A());
    

    编译器不知道f() 特别需要解决。因此,处理第二个块的编译器可以设置信息以供编译第一个块的输出使用,以决定如何调度任何给定的方法。

    但最终要到运行时才能决定实际执行给定的方法实现;无论是采用虚拟调度的形式,还是一些基于反射的怪物,或其他任何形式。

    在语言级别,这只是实现细节。指定的行为是重要的,这就是 virtualoverride 关键字之间的关系成为关键的地方。

    【讨论】:

      猜你喜欢
      • 2014-11-11
      • 2013-09-17
      • 2014-05-28
      • 1970-01-01
      • 1970-01-01
      • 2015-10-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多