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

Tar*_*rik 12 .net c# il

让我们说我们在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();
    }
  }
Run Code Online (Sandbox Code Playgroud)

当我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
Run Code Online (Sandbox Code Playgroud)

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

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

pho*_*oog 20

调用转到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();
Run Code Online (Sandbox Code Playgroud)

它可能会GetDerived()返回一个实例StillMoreDerived.如果StillMoreDerived(或继承链之间Derived和之间的任何类StillMoreDerived)重写该方法,则调用Derived该方法的实现将是不正确的.

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

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


Eri*_*ert 9

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

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

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