将调用什么功能?

时间:2018-09-06 12:56:04

标签: c# interface

以前,我提出的问题并未得到完全回答,因此,我决定重新制定我的问题,以了解发生的情况:

这是我的班级层次结构:

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. 如何确定要调用的函数的机制是什么?

4 个答案:

答案 0 :(得分:4)

在编译期间,它会将调用绑定到I接口,然后在运行期间,它将调用实现I.f()的继承链中的top方法。

因此,在您的代码中

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

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

答案 1 :(得分:4)

  

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

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

请注意,如果该方法为virtual,请参见@YeldarKurmangaliyev的答案,以了解如何调用“继承链中的顶级方法”(但这不是这里的情况)。 < / p>

  

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

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

  

一个类继承其基类提供的所有接口实现   类。如果不显式重新实现接口,则派生   类不能以任何方式更改其继承的接口映射   它的基类。

这就是class B : A显示 A class 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的解决方案更常见):

  

但是,当接口方法映射到虚拟方法中时,   类,派生类可能会覆盖虚拟类   方法并更改接口的实现。

答案 2 :(得分:1)

恕我直言,有趣的部分是为什么:((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();

当然,这是实际情况的简单版本,但有助于理解该概念

答案 3 :(得分:1)

我认为这尤其可能使像我一样了解C ++中的虚拟方法与非虚拟方法以及Java中的接口的人们感到困惑,后来又不得不弄清楚两者如何在C#中交互。 / p>

通过Java中的接口引用进行方法分派(相对)简单明了,因为Java中的所有方法都是虚拟的。因此,当然,实例运行时类型决定了要调用的内容,即... Java中的

这确实意味着实例,方法名称和实现之间的某些更复杂的关系在Java中不可用,根据您的观点,这可以说是“好事”还是“坏事”。 / p>

无论如何,这是Java和C#之间的最大区别之一,因为不仅不需要在C#中虚拟化方法,而且默认情况下也不是。无需深入了解实施细节,就可以得出关键的见解:

如果在父类中未将方法标记为virtual,则不能在子类中将相应的(名称和签名)方法标记为override。另外,如果子类中的方法未标记为override,则无论它是否在虚拟环境中,它的行为都像new 。基类

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

没关系。即使是这样,在C#中virtual并不意味着子类实现必须覆盖基类方法;它仅意味着它可以声明是否可以这样做。而且,通过不显式声明基类方法virtual,您将无法使用声明自身覆盖该方法的方法来编译子类。


Update :目前评分最高的答案声称,关键决定是在编译时做出的,而在特定示例中,实现可以做到这一点,不相信它会这样做-仅仅是因为这种方法不能一概而论。

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

编译上述方法时,编译器绝对不知道应调用什么实际实现。当用该行编译其他代码单元时

myMethod(new A());

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

但是最终要在运行时才能决定实际执行给定方法的实现。无论是采取虚拟调度的形式,还是基于反射的怪异形式,等等。

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