为什么C#编译器会生成方法调用以在IL中调用BaseClass方法

让我们说我们在C#中有以下示例代码:

class BaseClass { public virtual void HelloWorld() { Console.WriteLine("Hello Tarik"); } } class DerivedClass : BaseClass { public override void HelloWorld() { base.HelloWorld(); } } class Program { static void Main(string[] args) { DerivedClass derived = new DerivedClass(); derived.HelloWorld(); } } 

当我ildasmed以下代码时:

 .method private hidebysig static void Main(string[] args) cil managed { .entrypoint // Code size 15 (0xf) .maxstack 1 .locals init ([0] class EnumReflection.DerivedClass derived) IL_0000: nop IL_0001: newobj instance void EnumReflection.DerivedClass::.ctor() IL_0006: stloc.0 IL_0007: ldloc.0 IL_0008: callvirt instance void EnumReflection.BaseClass::HelloWorld() IL_000d: nop IL_000e: ret } // end of method Program::Main 

但是,csc.exe转换了derived.HelloWorld(); – > callvirt instance void EnumReflection.BaseClass::HelloWorld() 。 这是为什么? 我没有在Main方法的任何地方提到BaseClass。

而且如果它调用BaseClass::HelloWorld()那么我会期望call而不是callvirt因为它看起来直接调用BaseClass::HelloWorld()方法。

调用转到BaseClass :: HelloWorld,因为BaseClass是定义方法的类。 虚拟分派在C#中的工作方式是在基类上调用该方法,并且虚拟分派系统负责确保调用方法的最派生覆盖。

Eric Lippert的答案非常有用: https : //stackoverflow.com/a/5308369/385844

正如他关于这个主题的博客系列: http : //blogs.msdn.com/b/ericlippert/archive/tags/virtual+dispatch/

你知道为什么这样实现吗? 如果它直接调用派生类ToString方法会发生什么? 这种方式对我来说乍看之下并没有多大意义……

它是以这种方式实现的,因为编译器不跟踪对象的运行时类型,只跟踪它们的引用的编译时类型。 使用您发布的代码,很容易看到该调用将转到该方法的DerivedClass实现。 但假设derived变量初始化如下:

 Derived derived = GetDerived(); 

GetDerived()可能会返回StillMoreDerived的实例。 如果StillMoreDerived (或inheritance链中DerivedStillMoreDerived之间的任何类)覆盖该方法,则调用该方法的Derived实现将是不正确的。

要找到变量可以通过静态分析保持的所有可能值,就可以解决暂停问题。 使用.NET程序集时,问题更严重,因为程序集可能不是一个完整的程序。 因此,编译器可以合理地certificatederived不包含对更多派生对象(或空引用)的引用的情况的数量将很小。

添加此逻辑需要花多少钱才能发出call而不是callvirt指令? 毫无疑问,成本将远远高于所获得的小额效益。

考虑这个问题的方法是虚方法定义一个“槽”,您可以在运行时将方法放入其中。 当我们发出一个callvirt指令时,我们说“在运行时,看看这个槽中的内容并调用它”。

槽由有关声明虚方法的类型的方法信息标识,而不是覆盖它的类型。

向派生方法发出callvirt是完全合法的; 运行时会意识到派生方法与基本方法是相同的槽,结果将完全相同。 但是从来没有任何理由这样做。 如果我们通过识别声明该槽的类型来识别槽,则更清楚。

请注意,即使您将DerivedClass声明为已sealed也会发生这种情况。

C#使用callvirt运算符调用任何实例方法( virtual或非virtual )以自动获取对象引用的空检查 – 在调用方法时引发NullReferenceException 。 否则, NullReferenceException只会在方法内第一次实际使用该类的任何实例成员时引发,这可能会令人惊讶。 如果没有使用实例成员,则该方法实际上可以成功完成,而不会引发exception。

您还应该记住IL不是直接执行的。 它首先由JIT编译器编译为本机指令 – 并根据您是否正在调试该进程执行许多优化。 我发现用于CLR 2.0的x86 JIT内联了一个非虚方法但调用了虚方法 – 它还内联了Console.WriteLine