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()"); }
}
如您所见,C
与B
不同,除了从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#编译器完成的。
但是编译器如何在编译时决定
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
。之所以使用这种方法,是因为运行时能够自己判断目标方法何时为非虚拟方法。(另外,callvirt
对this
参数执行隐式null检查,而call
没有)
此IL转储应该回答您的所有其他问题。简而言之:编译器甚至不会尝试解析最后的方法调用。
静态绑定的含义与您想象的不同。它也被称为"早期绑定",与后期绑定相反,在c#版本4中可以使用动态关键字,在所有版本中都可以使用反射。延迟绑定的主要特点是,编译器甚至无法验证被调用的方法是否存在,更不用说验证是否传递了正确的参数。如果有什么是a-miss,你会得到一个运行时异常。它也很慢,因为运行时需要做额外的工作来查找方法、验证参数和构造调用堆栈帧。
当你使用接口或虚方法时,这不是一个问题,编译器可以预先验证一切。生成的代码非常高效。这仍然导致间接方法调用(又名"动态调度"),需要实现接口和虚拟方法,但在c#中仍然用于非虚拟实例方法。一个前c#团队成员在这篇博文中记录了这一点。使这项工作的CLR管道称为"方法表"。大致类似于c++中的v表,但是方法表包含每个方法的条目,包括非虚方法。接口引用就是指向该表的指针。