关于c#:将调用什么函数?

What function will be called?

之前我问了一个回答不充分的问题,因此我决定重新制定我的问题,以了解发生了什么:

这是我的类层次结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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");
    }
}

以下是执行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
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。

这里有一个问题:您能解释一下如何通过覆盖这些步骤来决定调用什么函数吗?

  • 当决定调用什么函数时——运行时还是编译时?
  • 如何决定调用哪个函数的机制是什么?

  • When the decision is made what function to call - runtime or compile
    time?

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

    请注意,如果该方法是virtual,那么请参见@yeldarkurmangaliyev的答案,了解它如何调用"继承链中的顶级方法"(但这不是这里的场景)。

    What is the mechanism how to decide what function to call?

    本规范的相关部分为13.4.5接口实现继承:

    A class inherits all interface implementations provided by its base
    classes. Without explicitly re-implementing an interface, a derived
    class cannot in any way alter the interface mappings it inherits from
    its base classes.

    这就是为什么class B : A显示a而class B : A, I显示b的原因。因为后者明确地重新实现了接口。

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

    A class inherits all interface implementations provided by its base
    classes. Without explicitly re-implementing an interface, a derived
    class cannot in any way alter the interface mappings it inherits from
    its base classes. For example, in the declarations

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    interface IControl
    {
        void Paint();
    }
    class Control: IControl
    {
        public void Paint() {...}
    }
    class TextBox: Control
    {
        new public void Paint() {...}
    }

    the Paint method in TextBox hides the Paint method in Control, but it
    does not alter the mapping of Control.Paint onto IControl.Paint, and
    calls to Paint through class instances and interface instances will
    have the following effects

    1
    2
    3
    4
    5
    6
    7
    8
    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更为常见的解决方案):

    However, when an interface method is mapped onto a virtual method in a
    class, it is possible for derived classes to override the virtual
    method and alter the implementation of the interface.


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

    所以,在你的代码中

    1
    2
    3
    4
    5
    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"。

    但是,当您这样做时:

    1
    2
    3
    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方法,例如具有不同名称的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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

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


    我认为这很可能混淆了(像我一样)在C++中了解虚拟与非虚拟方法的人,以及Java中的接口,后来必须弄清楚这两个如何在C语言中交互。

    通过Java中的接口引用进行方法调度是比较简单的,因为Java中的所有方法都是虚拟的。当然,实例运行时类型决定了调用什么,这就是…在Java中。

    这意味着在Java中,实例、方法名和实现之间的某些更复杂的关系是不可用的,这取决于您的观点,可以被认为是"好东西"或"坏东西"。

    不管怎样,这是Java和C语言之间的最大区别之一,因为不仅在C语言中不需要虚拟的方法,它们也不是默认的。而且,在不了解实施细节的情况下,这将带来关键的洞察力:

    如果一个方法在父类中没有被标记为virtual,那么在子类中就不能将对应的(同名和签名)方法标记为override。另外,如果子类中的方法没有标记为override,那么不管它是否在基类中是虚拟的,它都会表现为new

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

    但是:

    没关系。即使是这样,在c virtual中,并不意味着子类实现必须重写基类方法;它只是意味着如果声明可以这样做。如果不显式声明基类方法virtual,就不可能用声明自己是该方法重写的方法编译子类。

    更新:目前排名最高的答案声称关键决策是在编译时做出的,虽然在特定的示例中实现可以做到这一点,但我不相信这一点——仅仅是因为这种方法不能概括。

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

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

    1
    myMethod(new A());

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

    但最终,要真正执行给定的方法实现,必须等到运行时才能做出决定;无论是采用虚拟调度的形式,还是基于反射的怪兽,或者其他什么形式。

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


    imho,中间部分是为什么:((I)new B()).f()印刷品

    1
    ---->> A

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

    1
    2
    3
    4
    if (i is B)
       ((B)i).f();
    else
       i.f();

    当强制转换到I时,类声明就是这样进行的:

    1
    2
    3
    4
    5
    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:

    1
    class B : A, I

    因此,当投射到我身上时,这将发生:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 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();

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