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

时间:2012-04-18 22:47:21

标签: c# .net 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();
    }
  }

当我ild以下代码时:

.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()方法。

3 个答案:

答案 0 :(得分: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();

GetDerived()可能会返回StillMoreDerived的实例。如果StillMoreDerived(或继承链中DerivedStillMoreDerived之间的任何类)重写该方法,则调用该方法的Derived实现将是不正确的。 / p>

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

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

答案 1 :(得分:9)

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

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

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

答案 2 :(得分:1)

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

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

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