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
检查。
看一个简单的例子:
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 代码如下:
点击查看 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 代码如下:
点击查看 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
类型的实例,不知道这样理解对不对),而没有去检测调用对象的实际类型,所以调用的 Object
的 ToString
方法。