12

假设我们在 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()方法。

4

3 回答 3

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任何类)覆盖该方法,则调用该方法的实现是不正确的。DerivedStillMoreDerivedDerived

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

添加此逻辑以便它可以发出指令call而不是callvirt指令需要多少成本?毫无疑问,成本将远远高于所获得的微小收益。

于 2012-04-18T23:08:39.853 回答
9

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

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

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

于 2012-04-19T04:34:42.557 回答
1

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

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

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

于 2013-04-09T22:38:20.090 回答