C# call & callvirt 指令

先来看看两个指令的解释 (摘自 《CLR Via C# 第 4 版》 ):

看一个简单的例子:

static void Main(string[] args)
{
    Console.WriteLine(); // 静态方法

    Object o = new Object();
    o.GetHashCode(); // 虚方法
    o.GetType(); // 非虚方法
}
  

附一下 Object 类的定义:

public class Object
{
    public Object();
    public virtual bool Equals(object obj);
    public static bool Equals(object objA, object objB);
    public virtual int GetHashCode();
    public Type GetType();
    protected object MemberwiseClone();
    public static bool ReferenceEquals(object objA, object objB);
    public virtual string ToString();
}
  

上面例子的 IL 代码如下:

.method private hidebysig static void  Main(string[] args) cil managed
{
    .entrypoint
    // 代码大小       28 (0x1c)
    .maxstack  1
    .locals init ([0] object o)
    IL_0000:  nop
    // 使用 call 指令调用 WriteLine 静态方法
    IL_0001:  call       void [mscorlib]System.Console::WriteLine()
    IL_0006:  nop
    // 调用 Object 的构造函数
    IL_0007:  newobj     instance void [mscorlib]System.Object::.ctor()
    IL_000c:  stloc.0
    IL_000d:  ldloc.0
    // 使用 callvirt 指令调用 Object 的虚方法
    IL_000e:  callvirt   instance int32 [mscorlib]System.Object::GetHashCode()
    IL_0013:  pop
    IL_0014:  ldloc.0
    // 使用 callvirt 指令调用 Object 的实例非虚方法
    IL_0015:  callvirt   instance class [mscorlib]System.Type [mscorlib]System.Object::GetType()
    IL_001a:  pop
    IL_001b:  ret
} // end of method Program::Main
  

第一个调用使用 call 指令调用静态方法;

第二个调用使用 callvirt 调用虚方法。这两个都很正常。

第三个调用 o.GetType() 使用了 callvirt 调用了非虚方法,略显奇怪。

这个是因为 C# 的约定,固定使用 callvirt 调用非虚方法。

可以看一下下面的例子:

class Program
{
    public Int32 GetFive() { return 5; }
    static void Main(string[] args)
    {
        Program p = null;
        p.GetFive();
    }
}
  

非虚方法 GetFive 中没有使用任何实例变量,应该能正常执行才对,但是编译会出 System.NullReferenceException 异常。

未将对象引用设置到对象的实例。

这就是因为 C# 使用 callvirt 指令调用非虚方法 GetFive ,而不是 call 指令的证明。

那什么场合用 call 指令来调用虚方法呢?再来看一个例子:

internal class SomeClass
{
    public override string ToString()
    {
        // 编译器使用 IL 指令 call, 以非虚方式调用 Object 的 ToString 方法
        // 如果编译器用 callvirt 而不是 call 指令,那么该方法将递归调用自身,直到栈溢出
        return base.ToString();
    }
}
  

IL 代码如下:

.method public hidebysig virtual instance string 
      ToString() cil managed
{
    // 代码大小       12 (0xc)
    .maxstack  1
    .locals init ([0] string CS$1$0000)
    IL_0000:  nop
    IL_0001:  ldarg.0
    IL_0002:  call       instance string [mscorlib]System.Object::ToString()
    IL_0007:  stloc.0
    IL_0008:  br.s       IL_000a

    IL_000a:  ldloc.0
    IL_000b:  ret
} // end of method SomeClass::ToString
  

这里有点难理解,以下是我的理解,不知道对不对。先看一下对 callvirt 的说明:

callvirt 指令调用虚实例方法,CLR 调查发出调用的对象的实际类型,然后以多态方式调用方法。

使用 callvirt 时 CLR 会调查发出调用的对象的实际类型(这里即 SomeClass 类型),然后以多态方式调用方法(即调用 SomeClass 中重写的 ToString 方法);

而使用 call 调用虚方法时,调用变量本身的类型指明了方法的定义类型(这里调用变量是 base ,根据我的理解 base 应该是指向 Object 类型的实例,不知道这样理解对不对),而没有去检测调用对象的实际类型,所以调用的 ObjectToString 方法。