CIL代码中调用方法的常用方法有call和callvirt两种,那它们到底有什么区别呢?下面让我们通过例子来看一看吧!
CIL代码准备工作
首先定义一个名为Person的基类,其中有一个介绍自己的虚方法,然后从Person派生出Guo和Yoyo,而且对其中介绍自己的方法做了如代码中的处理,一个在CIL的层面上未作处理(其实是省略了.override),另一个为虚方法增加了newslot属性。
.namespace ConsoleTest
{
.class public Person
{
.method public void .ctor()
{
.maxstack 1
ldarg.0
call instance void [mscorlib]System.Object::.ctor()
ret
}
.method public virtual void Introduce()
{
.maxstack 1
ldstr "this is Person"
call void [mscorlib]System.Console::WriteLine(string)
ret
}
}
.class public Guo extends ConsoleTest.Person
{
.method public void .ctor()
{
.maxstack 1
ldarg.0
call instance void [mscorlib]System.Object::.ctor()
ret
}
.method public virtual void Introduce()
{
.maxstack 1
ldstr "this is Guo"
call void [mscorlib]System.Console::WriteLine(string)
ret
}
}
.class public Yoyo extends ConsoleTest.Person
{
.method public void .ctor()
{
.maxstack 1
ldarg.0
call instance void [mscorlib]System.Object::.ctor()
ret
}
//此处使用newslot属性或者说标签,标志着该方法脱离了基类虚函数的那一套链,等同于C#中的new
.method public newslot virtual void Introduce()
{
.maxstack 1
ldstr "this is Yoyo"
call void [mscorlib]System.Console::WriteLine(string)
ret
}
}
}
编译时类型和运行时类型
在进行下文之前先要介绍一下编译时类型和运行时类型这两个概念,因为这和我们的方法调用息息相关。
举个C#的例子来说明这个问题:
public abstract class Band{ }
public class Beyond:Band{ }
class Program
{
public static void Main(string[] args)
{
Band obj = new Beyond();
}
}
对于编译器来说,变量的类型就是它被声明时的类型。在此变量obj的类型被定义为Band,也就是说obj的编译时类型是Band。
但我们之后实例化了一个Beyond类型的实例,并且将这个实例的引用赋值给了变量obj,因此在这段程序运行的时候,编译阶段被定义为Band类型的变量obj指向的是一块存储了Beyond类型的实例的内存。换言之,此时obj的运行时类型的Beyond。
那么编译时类型和运行时类型跟我们上面的CIL代码有什么关系呢?下面让我们通过对比来看一下!
call vs callvirt
下面我们将使用CIL代码来实现这个对比。
首先我们要声明3个局部变量来分别存储这三个类的实例,这3个变量是作为运行时类型存在的。
其次分别使用call和callvirt来调用各个类的方法,此时调用的类的类型充当的是编译时类型。
.method public static void Main(string[] args)
{
.entrypoint
.maxstack 3
.locals init(
class ConsoleTest.Person objPerson,
class ConsoleTest.Guo objGuo,
class ConsoleTest.Yoyo objYoyo
)
newobj instance void ConsoleTest.Person::.ctor()
stloc objPerson
newobj instance void ConsoleTest.Guo::.ctor()
stloc objGuo
newobj instance void ConsoleTest.Yoyo::.ctor()
stloc objYoyo
//Person
//编译时类型为Console.Person,运行时类型为Console.Person,使用call
ldloc objPerson
call instance void ConsoleTest.Person::Introduce()
//编译时类型为Console.Person,运行时类型为Console.Person,使用callvirt
ldloc objPerson
callvirt instance void ConsoleTest.Person::Introduce()
//Guo
//编译时类型为Console.Guo,运行时类型为Console.Guo,使用call
ldloc objGuo
call instance void ConsoleTest.Guo::Introduce()
//编译时类型为Console.Guo,运行时类型为Console.Guo,使用callvirt
ldloc objGuo
callvirt instance void ConsoleTest.Guo::Introduce()
//编译时类型为Console.Person,运行时类型为Console.Guo,使用call
ldloc objGuo
call instance void ConsoleTest.Person::Introduce()
//编译时类型为Console.Person,运行时类型为Console.Guo,使用callvirt
ldloc objGuo
callvirt instance void ConsoleTest.Person::Introduce()
//Yoyo
//编译时类型为Console.Yoyo,运行时类型为Console.Yoyo,使用call
ldloc objYoyo
call instance void ConsoleTest.Yoyo::Introduce()
//编译时类型为Console.Yoyo,运行时类型为Console.Yoyo,使用callvirt
ldloc objYoyo
callvirt instance void ConsoleTest.Yoyo::Introduce()
//编译时类型为Console.Person,运行时类型为Console.Yoyo,使用call
ldloc objYoyo
call instance void ConsoleTest.Person::Introduce()
//编译时类型为Console.Person,运行时类型为Console.Yoyo,使用callvirt
ldloc objYoyo
callvirt instance void ConsoleTest.Person::Introduce()
call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
pop
ret
}
好了,我们PK的擂台已经搭好了。如果有兴趣的话,各位此时可以对照各个方法猜一下输出结果。
不过在正式揭晓结局之前,我先了总结一下这个过程:Person类作为基类,有一个虚函数Introduce。然后Guo类派生自Person,同时Guo类也有一个同名的虚函数Introduce,此时可以认为它重载了基类的同名方法。我为了对比的更有趣,又定义了一个派生自Person的Yoyo类,同样它也有一个同名的虚函数Introduce,唯一的不同就是此方法使用了newslot属性。
下面我们来揭晓结果:
我们将代码和结果一一对应,可以发现凡是使用call调用方法的:
- call instance void ConsoleTest.Person::Introduce() 编译时类型Person,运行时类型Person。输出:this is Person,调用了Person中定义的Introduce方法
- call instance void ConsoleTest.Guo::Introduce() 编译时类型Guo,运行时类型Guo。输出:this is Guo,调用了Guo中定义的Introduce方法
- call instance void ConsoleTest.Person::Introduce() 编译时类型Person,运行时类型Guo。输出:this is Person,调用了Person中定义的Introduce方法
- call instance void ConsoleTest.Yoyo::Introduce() 编译时类型Yoyo,运行时类型Yoyo。输出:this is Yoyo,调用了Yoyo中定义的Introduce方法
- call instance void ConsoleTest.Person::Introduce() 编译时类型Person,运行时类型Yoyo。输出:this is Person,调用了Person中定义的Introduce方法
从实际运行结果可以看出,call对变量的运行时类型不感兴趣,只对编译时类型的方法感兴趣,所以此处call只会调用变量编译时类型中定义的方法。为了方便大家观察结果,我已经把对应的关键字加粗。
而使用了callvirt来调用方法的:
- callvirt instance void ConsoleTest.Person::Introduce() 编译时类型Person,运行时类型Person。输出:this is Person,调用了Person中定义的Introduce方法
- callvirt instance void ConsoleTest.Guo::Introduce() 编译时类型Guo,运行时类型Guo。输出:this is Guo,调用了Guo中定义的Introduce方法
- callvirt instance void ConsoleTest.Person::Introduce() 编译时类型Person,运行时类型Guo。输出:this is Guo,调用了Guo中定义的Introduce方法
- callvirt instance void ConsoleTest.Yoyo::Introduce() 编译时类型Yoyo,运行时类型Yoyo。输出:this is Yoyo,调用了Yoyo中定义的Introduce方法
- callvirt instance void ConsoleTest.Person::Introduce() 编译时类型Person,运行时类型Yoyo。输出:this is Person,调用了Person中定义的Introduce方法
从实际运行结果可以看出,callvirt对变量的编译时类型不感兴趣,只对运行时类型的方法感兴趣,所以此处callvirt只会调用变量运行时类型中定义的方法。为了方便大家观察结果,我已经把对应的关键字加粗。
不过有一个例外,当编译时类型为Person,运行时类型为Guo,使用callvirt去调用Person::Introduce()时,执行的是子类Guo中的Introduce方法;
当编译时类型为Person,运行时类型为Yoyo,使用callvirt去调用Person::Introduce()时,执行的却是基类Person中定义的Introduce方法;
这是因为Yoyo类中的Introduce方法使用了newslot属性,所以此处才看到了截然不同的结果。
这个涉及到了虚函数的设计,简单来说可以想象成同一系列的虚函数(使用override关键字修饰)存放在一个槽(slot)中,在运行时会将没有使用newslot属性的虚函数放入这个槽中,在需要调用虚函数时就去这个槽中寻找符合条件的虚函数执行,那这个槽是谁定义的呢或者说如何去定位正确的槽呢?不错,就是通过基类。
如果有兴趣,各位可以把虚函数部分的C#代码编译成CIL代码,这样就能看到调用派生类重载的虚函数,在CIL中其实都是使用callvirt instance xxx baseclass:func 来实现的。所以使用了newslot属性的方法并没有放入到基类定义的那个槽中,而是自己定义了一个新的槽,所以
ldloc objYoyo
callvirt instance void ConsoleTest.Person::Introduce()
只能调用基类中的Introduce方法。
解惑:CIL代码中的虚函数
先写一段C#代码:
namespace CILTest
{
public class Program
{
public static void Main(string[] args)
{
Person objPerson1 = new Guo();
objPerson1.Introduce();
Person objPerson2 = new Yoyo();
objPerson2.Introduce();
Guo objGuo = new Guo();
objGuo.Introduce();
Yoyo objYoyo = new Yoyo();
objYoyo.Introduce();
Console.ReadKey();
}
}
public class Person
{
public Person()
{
}
public virtual void Introduce()
{
Console.WriteLine("this is Person");
}
}
public class Guo:Person
{
public Guo()
{
}
public override void Introduce()
{
Console.WriteLine("this is Guo");
}
}
public class Yoyo : Person
{
public Yoyo()
{
}
public new void Introduce()
{
Console.WriteLine("this is Yoyo");
}
}
}
Main方法对应的CIL代码截图如下:
通过分析CIL代码发现,调用派生类重载的虚函数,确实是使用callvirt instance xxx baseclass:func 来实现的。如果派生类重载的虚函数加了new关键字,那就会变为使用callvirt instance xxx class:func来实现。
发放源码
对比call和callvirt所用到的完整的il代码如下:
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 )
.ver 4:0:0:0
}
.assembly GuoAssembly
{
.ver 0:0:0:0
}
.module GuoModule
.namespace ConsoleTest
{
.class public Program extends [mscorlib]System.Object
{
.method public void .ctor()
{
.maxstack 8
ldarg.0
call instance void [mscorlib]System.Object::.ctor()
ret
}
.method public static void Main(string[] args)
{
.entrypoint
.maxstack 3
.locals init(
class ConsoleTest.Person objPerson,
class ConsoleTest.Guo objGuo,
class ConsoleTest.Yoyo objYoyo
)
newobj instance void ConsoleTest.Person::.ctor()
stloc objPerson
newobj instance void ConsoleTest.Guo::.ctor()
stloc objGuo
newobj instance void ConsoleTest.Yoyo::.ctor()
stloc objYoyo
ldloc objPerson
call instance void ConsoleTest.Person::Introduce()
ldloc objPerson
callvirt instance void ConsoleTest.Person::Introduce()
ldloc objGuo
call instance void ConsoleTest.Guo::Introduce()
ldloc objGuo
callvirt instance void ConsoleTest.Guo::Introduce()
ldloc objGuo
call instance void ConsoleTest.Person::Introduce()
ldloc objGuo
callvirt instance void ConsoleTest.Person::Introduce()
ldloc objYoyo
call instance void ConsoleTest.Yoyo::Introduce()
ldloc objYoyo
callvirt instance void ConsoleTest.Yoyo::Introduce()
ldloc objYoyo
call instance void ConsoleTest.Person::Introduce()
ldloc objYoyo
callvirt instance void ConsoleTest.Person::Introduce()
call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey()
pop
ret
}
}
.class public Person
{
.method public void .ctor()
{
.maxstack 1
ldarg.0
call instance void [mscorlib]System.Object::.ctor()
ret
}
.method public virtual void Introduce()
{
.maxstack 1
ldstr "this is Person"
call void [mscorlib]System.Console::WriteLine(string)
ret
}
}
.class public Guo extends ConsoleTest.Person
{
.method public void .ctor()
{
.maxstack 1
ldarg.0
call instance void [mscorlib]System.Object::.ctor()
ret
}
.method public virtual void Introduce()
{
.maxstack 1
ldstr "this is Guo"
call void [mscorlib]System.Console::WriteLine(string)
ret
}
}
.class public Yoyo extends ConsoleTest.Person
{
.method public void .ctor()
{
.maxstack 1
ldarg.0
call instance void [mscorlib]System.Object::.ctor()
ret
}
.method public newslot virtual void Introduce()
{
.maxstack 1
ldstr "this is Yoyo"
call void [mscorlib]System.Console::WriteLine(string)
ret
}
}
}