2

我知道非虚拟方法是静态绑定的,据我所知,这意味着它在编译时本身就知道将在哪个对象上调用哪个方法。该决定是根据对象的静态类型做出的。让我感到困惑的是接口(而不是class)和静态绑定。

考虑这段代码,

public interface IA
{
    void f();
}
public class A : IA
{
    public void f() {  Console.WriteLine("A.f()"); }
}
public class B : A 
{
    public new void f() {  Console.WriteLine("B.f()"); }
}

B b = new B();
b.f();  //calls B.f()     //Line 1

IA ia = b as IA;
ia.f(); //calls A.f()     //Line 2

演示代码:http: //ideone.com/JOVmi

我明白了Line 1。编译器可以知道b.f()将调用它,B.f()因为它知道它的静态类型bB.

但是编译器如何在编译时自己决定ia.f()调用A.f()? 什么是对象的静态类型ia?不是IA吗?但那是一个接口,并且没有任何f(). 那么它是如何工作的呢?

为了使案例更令人费解,让我们考虑这种static方法:

static void g(IA ia)
{
   ia.f(); //What will it call? There can be too many classes implementing IA!
}

正如评论所说,实现接口的类可能太多IA,那么编译如何静态决定ia.f()调用哪个方法?我的意思是,假设我有一个类定义为:

public class C : A, IA 
{
    public new void f() { Console.WriteLine("C.f()"); }
}

如您所见,C与 不同,除了派生于 之外,还B实现了. 这意味着,我们在这里有不同的行为:IAA

g(new B()); //inside g(): ia.f() calls A.f() as before!
g(new C()); //inside g(): ia.f() doesn't calls A.f(), rather it calls C.f()

演示代码:http: //ideone.com/awCor

我如何理解所有这些变化,尤其是接口和静态绑定如何协同工作?

还有更多(ideone):

C c = new C();
c.f(); //calls C.f()

IA ia = c as IA;
ia.f(); //calls C.f()

A a = c as A;
a.f(); //doesn't call C.f() - instead calls A.f()

IA iaa = a as IA;
iaa.f(); //calls C.f() - not A.f()

请帮助我理解所有这些,以及 C# 编译器如何完成静态绑定。

4

2 回答 2

6

但是编译器如何在编译时自己决定ia.f()调用A.f()?

它没有。它知道这ia.f()将调用IA.f()包含在ia. 它发出这个调用操作码,并让运行时在调用执行时找出它。

这是将为示例代码的下半部分发出的 IL:

    .locals init (
            class B   V_0,
            class IA  V_1)
    IL_0000:  newobj instance void class B::'.ctor'()
    IL_0005:  stloc.0
    IL_0006:  ldloc.0
    IL_0007:  callvirt instance void class B::f()
    IL_000c:  ldloc.0
    IL_000d:  stloc.1
    IL_000e:  ldloc.1
    IL_000f:  callvirt instance void class IA::f()
    IL_0014:  ret

请注意,callvirt在这两种情况下都使用。使用它是因为运行时能够自行确定目标方法何时是非虚拟的。(此外,callvirt对参数执行隐式 null 检查this,而call不会。)

这个 IL 转储应该回答你所有的其他问题。简而言之:编译器甚至不会尝试解析最终的方法调用。这是运行时的工作。

于 2011-08-27T18:24:30.413 回答
1

静态绑定的含义与您想象的不同。也称为“早期绑定”,它与后期绑定相反,可在 C# 版本 4 中使用dynamic关键字和在所有版本中使用反射。后期绑定的主要特点是编译器无法验证被调用的方法是否存在,更不用说验证是否传递了正确的参数。如果有什么问题,你会得到一个运行时异常。它也很慢,因为运行时需要做额外的工作来查找方法、验证参数和构造调用堆栈帧。

当您使用接口或虚拟方法时,这不是问题,编译器可以预先验证所有内容。生成的代码非常有效。这仍然会导致实现接口和虚拟方法所需的间接方法调用(也称为“动态调度”),但在 C# 中仍用于非虚拟实例方法。前 C# 团队成员在此博客文章中记录。完成这项工作的 CLR 管道称为“方法表”。大致类似于 C++ 中的 v-table,但方法表包含每个方法的条目,包括非虚拟方法。接口引用只是指向该表的指针。

于 2011-08-27T20:03:28.447 回答