转:MSIL: call & callvirt

阅读下面的代码,直接说出输出结果。
class One
{
   private int x = 1;
   public virtual void Test()
   {
     Console.WriteLine("One:" + x);
   }
}

class Two : One
{
   private int x = 2;
   public new void Test()
   {
     Console.WriteLine("Two:" + x);
   }
}

class Three : Two
{
}

class Program
{
   static void Main(string[] args)
   {
     var o = new Three();
     o.Test();

     (o as Two).Test();
     (o as One).Test();
   }
}

输出:
Two:2
Two:2
One:1

嗯~~~~ 这是一个不该在正式项目中出现的 "糟糕" 多态案例,因为它违反了 LSP 原则 (Liskov: 子类型必须能够替换掉他们的基类型)。我们在此处使用它的目的仅仅是为了更好地理解 Virtual Method、NonVirtual Method、call、callvirt 等知识,以便规避多态这个 OOP 重要理论中存在的某些风险和陷阱。

首先让我们回忆一下几个基本概念:

1. 虚方法会插入到当前及继承类型的虚方法表 (v-table) 中。
2. call 指令直接通过指令参数所指定的方法类型完成调用,而 callvirt 则会检查对象实际类型和它的虚方法表。
3. 使用对象引用 (reference) 调用非虚方法时,也会使用 callvirt 指令,因为它会检查引用是否为 null。
4. 源码编译时,非虚方法总是 "call 最近类型::NonVirtualMethod",而虚方法则是 "callvirt 最远类型::VirtualMethod"。
5. 运行时 callvirt 会通过 v-table 调用 "最近的虚方法版本"。

好了,对照这个规则,我们看看 Main() 所生成的 IL 代码。
.entrypoint
.maxstack 1
.locals init ([0] class Three o)

IL_0000: nop
IL_0001: newobj instance void Three::.ctor() // var o = new Three();
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: callvirt instance void Two::Test() // o.Test(); --> Two:2
IL_000d: nop
IL_000e: ldloc.0
IL_000f: callvirt instance void Two::Test() // (o as Two).Test(); --> Two:2
IL_0014: nop
IL_0015: ldloc.0
IL_0016: callvirt instance void One::Test() // (o as One).Test(); --> One:1

由于本例中都是使用对象引用,因此没有出现 call 指令。第一个疑问可能来自 IL_0008 这行的编译结果,这是因为 class Two 中使用 new 修饰关键字隐藏了基类型 One 的虚方法 Test,也就是说从这开始 Test 已经变成了一个非虚方法。验证一下:
Console.WriteLine(typeof(One).GetMethod("Test").IsVirtual); // True
Console.WriteLine(typeof(Two).GetMethod("Test").IsVirtual); // False
Console.WriteLine(typeof(Three).GetMethod("Test").IsVirtual); // False

既然是非虚方法,对照上面的规则,编译器自然选择一个 "距离最近" 的基类定义,也就是 "Two::Test"。如此一来,IL_0008 和 IL_000f 输出 "Two:2" 也就不奇怪了。当执行到 IL_0016 时,"One.Test" 可是一个 "纯正" 的虚方法,因此 callvirt 会检查对象 o 的实际类型 Three 的 v-table。
Slots in VTable: 6
--------------------------------------
MethodDesc Table

79371278   7914b928   PreJIT   System.Object.ToString()
7936b3b0   7914b930   PreJIT   System.Object.Equals(System.Object)
7936b3d0   7914b948   PreJIT   System.Object.GetHashCode()
793624d0   7914b950   PreJIT   System.Object.Finalize()
0099c080   009930b8   JIT     One.Test()
0099c100   009931b0   JIT     Three..ctor()

"距离最近的虚方法版本" 自然是 "One.Test" 了。我们试着修改一下上面的例子,以确认一下是否真的会选择距离最近的版本。
class One
{
   private int x = 1;
   public virtual void Test()
   {
     Console.WriteLine("One:" + x);
   }
}

class Two : One
{
   private int x = 2;
   public override void Test()
   {
     Console.WriteLine("Two:" + x);
   }
}

class Three : Two
{
}

class Program
{
   static void Main(string[] args)
   {
     var o = new Three();
     (o as One).Test(); // Two: 2
   }
}

Main() 的反编译代码: 距离最远的定义
IL_0000: nop
IL_0001: newobj instance void Three::.ctor()
IL_0006: stloc.0
IL_0007: ldloc.0
IL_0008: callvirt instance void One::Test()

调试器的跟踪结果: 距离最近的虚方法版本
79371278   7914b928   PreJIT   System.Object.ToString()
7936b3b0   7914b930   PreJIT   System.Object.Equals(System.Object)
7936b3d0   7914b948   PreJIT   System.Object.GetHashCode()
793624d0   7914b950   PreJIT   System.Object.Finalize()
0045c0d0   00453140   JIT     Two.Test()
0045c100   004531a8   JIT     Three..ctor()

而在某些时候,编译器会使用 call 指令代替 callvirt 调用虚方法。
class One
{
   public virtual void Test() { }
}

class Two : One
{
   public override sealed void Test()
   {
     base.Test();
   }
}

class Three : Two
{
   public void Call()
   {
     base.Test();
   }
}

看看编译后的 IL 代码。
.class private auto ansi beforefieldinit One
   extends [mscorlib]System.Object
{
}

.class private auto ansi beforefieldinit Two
   extends One
{
   .method public hidebysig virtual final instance void Test() cil managed
   {
     .maxstack 8
     L_0000: nop
     L_0001: ldarg.0
     L_0002: call instance void One::Test()
     L_0007: nop
     L_0008: ret

     /*
       如果使用 callvirt 指令,那么通过 v-table 找到的 "距离最近的版本",
       就是自身这个 override virtual method,
       callvirt self 的结果就是陷入无限递归死循环 ~~~~~

       Name: Two
       Slots in VTable: 6
       --------------------------------------
       MethodDesc Table
       79371278   7914b928   PreJIT   System.Object.ToString()
       7936b3b0   7914b930   PreJIT   System.Object.Equals(System.Object)
       7936b3d0   7914b948   PreJIT   System.Object.GetHashCode()
       793624d0   7914b950   PreJIT   System.Object.Finalize()
       0099c0b0   00993110   JIT     Two.Test()
       0099c0c0   00993118   JIT     Two..ctor()
     */
   }
}

.class private auto ansi beforefieldinit Three
   extends Two
{
   .method public hidebysig instance void Call() cil managed
   {
     .maxstack 8
     L_0000: nop
     L_0001: ldarg.0
     L_0002: call instance void Two::Test()
     L_0007: nop
     L_0008: ret

     /*
       virtual method Test() 在 class Two 中变成了 sealed 方法,
       那么最近的版本自然也就是它了,
       直接使用 call 自然也就比 callvirt 效率高出很多。
     */
   }

你可能感兴趣的:(call)