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 方法。

Page Layout Max Width

Adjust the exact value of the page width of VitePress layout to adapt to different reading needs and screens.

Adjust the maximum width of the page layout
A ranged slider for user to choose and customize their desired width of the maximum width of the page layout can go.

Content Layout Max Width

Adjust the exact value of the document content width of VitePress layout to adapt to different reading needs and screens.

Adjust the maximum width of the content layout
A ranged slider for user to choose and customize their desired width of the maximum width of the content layout can go.