Skip to content

C# call & callvirt 指令

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

call

该 IL 指令可调用静态方法、实例方法和虚方法。用 call 指令调用静态方法,必须指定方法的定义类型。用 call 指令调用实例方法或虚方法,必须指定引用了对象的变量。call 指令假定该变量不为 null。换言之,变量本身的类型指明了方法的定义类型。如果变量的类型没有定义该方法,就检查基类型来查找匹配方法。call 指令经常用于以非虚方式调用虚方法。

callvirt

该 IL 指令可调用实例方法和虚方法,不能调用静态方法。用 callvirt 指令调用实例方法或虚方法,必须指定引用了对象的变量。用 callvirt 指令调用非虚实例方法,变量的类型指明了方法的定义类型。用 callvirt 指令调用虚实例方法,CLR 调查发出调用的对象的实际类型,然后以多态方式调用方法。为了确定类型,发出调用的变量对不能为 null。换言之,编译这个调用时,JIT 编译器会生成代码来验证变量的值是不是为 null。正是由于要进行这种额外的检查,所有 callvirt 指令执行速度比 call 稍慢。注意,即使 callvirt 指令调用的是一个非虚的实例方法,也会执行这种 null 检查。

看一个简单的例子:

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

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

附一下 Object 类的定义:

csharp
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 代码如下:

点击查看 IL 代码
cs
.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 调用非虚方法。

可以看一下下面的例子:

csharp
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 指令来调用虚方法呢?再来看一个例子:

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

IL 代码如下:

点击查看 IL 代码
cs
.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 方法。