c#中的非虚方法、静态绑定和接口

本文关键字:静态绑定 接口 方法 | 更新日期: 2023-09-27 18:07:01

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

考虑下面的代码,

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(),因为它知道b静态类型是B

但是编译器如何在编译时决定 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()"); }
}

如您所见,CB不同,除了从A派生之外,还实现了IA。这意味着,我们在这里有一个不同的行为:

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

我该如何理解所有这些变化,特别是接口和静态绑定是如何一起工作的?

And few more (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#编译器完成的。

c#中的非虚方法、静态绑定和接口

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

它不。它知道ia.f()将在ia中包含的对象实例上调用IA.f()。它发出这个调用操作码,并让运行时在执行调用时找出它。

下面是将为示例代码的下半部分发出的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。之所以使用这种方法,是因为运行时能够自己判断目标方法何时为非虚拟方法。(另外,callvirtthis参数执行隐式null检查,而call没有)

此IL转储应该回答您的所有其他问题。简而言之:编译器甚至不会尝试解析最后的方法调用。

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

当你使用接口或虚方法时,这不是一个问题,编译器可以预先验证一切。生成的代码非常高效。这仍然导致间接方法调用(又名"动态调度"),需要实现接口和虚拟方法,但在c#中仍然用于非虚拟实例方法。一个前c#团队成员在这篇博文中记录了这一点。使这项工作的CLR管道称为"方法表"。大致类似于c++中的v表,但是方法表包含每个方法的条目,包括非虚方法。接口引用就是指向该表的指针。