用CIL写程序:从“call vs callvirt”看方法调用
前文回顾:《用CIL写程序系列》
前言:
最近的时间都奉献给了加班,距离上一篇文章也有半个多月了。不过在上一篇文章《用CIL写程序:定义一个叫“慕容小匹夫”的类》中,匹夫和各位看官一起用CIL语言定义了一个类,并且在实例化之后给各位拜了大年。但是那篇文章中,匹夫还是留下了一个小坑,那就是关于调用方法时,CIL究竟应该使用call呢还是应该使用callvirt呢?看上去是一个很肤浅的问题,哪个能让程序跑起来哪个就是好的嘛。不是有一句话:白猫黑猫,抓到耗子就是好猫嘛。不过其实这并不是一个很表面的问题,如果深入挖掘的确会有一些额外的收获,凡事都有因有果。那么匹夫就和各位一起去分析下这个话题背后的故事吧~~
一段“本应报错”的代码
虽然题目叫所谓的的用CIL写程序,但匹夫的目的其实并非是写CIL代码,而是通过写CIL代码来使各位对CIL的认识更加清晰,一个好脑瓜抵不过一个烂笔头嘛。所以写的都是.il作为后缀的文件,而没有写过.cs作为后缀的文件。不过为了响应上一篇文章中有园友建议加入ILGenerator的部分,匹夫决定就从本篇开篇引入一段使用了ILGenerator的代码。
- //
- using System;
- using System.Reflection;
- using System.Reflection.Emit;
- public class Test1
- {
- delegate void HelloDelegate(Murong murong);
- public static void Main(string[] args)
- {
- Murong murong = null;//注意murong是null哦~
- Type[] helloArgs = {typeof(Murong)};
- var hello = new DynamicMethod("Hello",
- typeof(void), helloArgs,
- typeof(Murong).Module);
- ILGenerator il = hello.GetILGenerator();
- il.Emit(OpCodes.Ldarg_0);
- var foo = typeof(Murong).GetMethod("Foo");
- il.Emit(OpCodes.Call, foo);
- il.Emit(OpCodes.Ret);
- var print = (HelloDelegate)hello.CreateDelegate(typeof(HelloDelegate));
- print(murong);
- }
- internal class Murong
- {
- //注意Foo不是静态方法额~
- public void Foo()
- {
- Console.WriteLine("this == null is " + (this == null));
- }
- }
- }
如果按照“理性的分析”,你要调用一个类中不是静态的方法,那你肯定要先拿到它的实例引用吧。也就是murong不能是null吧?否则就成了null.Foo(),按理说会报空指针的错误(NullReferenceException
)。可是呢?我们编译并且运行一下看看。
答案竟然是没有报错。而且的确调用到了Foo方法并且打印出了“this == null is True”。而且this的确是null,Murong这个类并没有被实例化。可Foo这个方法可是一个实例方法啊。实例是null怎么可能会调用的到它?
call到底是个什么鬼?为什么不检测实例到底是否为null就能直接调用方法呢?
下面让我们带着上文的疑问,再去看一段也很有趣的代码,同时收获新的的困惑。
虚函数的奇怪事
各位园友、看官想必对C#的虚函数是什么都十分熟悉,作为面向对象的语言,虚函数这个概念的存在是必要的,匹夫在此也就不再过多介绍了。
既然各位都熟悉C#的虚函数,那小匹夫在此直接使用CIL实现虚函数,想必各位也会十分快速的理解。那么好,在此匹夫会定义一个叫People的类作为基类,其中有一个介绍自己的虚方法。同时分别从People派生了两个类Murong和ChenJD,而且对其中介绍自己的方法做了如代码中的处理,一个使用在CIL的层面上未做处理(其实是省略了.override),另一个方法匹夫为它增加了newslot属性。
- //如何用CIL声明一个类,请看小匹夫的上一篇文章《用CIL写程序:定义一个叫“慕容小匹夫”的类》
- .class People
- {
- .method public void .ctor()
- {
- .maxstack
- ldarg. //1.将实例的引用压栈
- call instance void [mscorlib]System.Object::.ctor() //2.调用基类的构造函数
- ret
- }
- .method public virtual void Introduce()
- {
- .maxstack
- ldstr "我是People"
- call void [mscorlib]System.Console::WriteLine(string)
- ret
- }
- }
- .class Murong extends People
- {
- .method public void .ctor()
- {
- .maxstack
- ldarg. //1.将实例的引用压栈
- call instance void [mscorlib]System.Object::.ctor() //2.调用基类的构造函数
- ret
- }
- .method public virtual void Introduce()
- {
- .maxstack
- ldstr "我是慕容小匹夫"
- call void [mscorlib]System.Console::WriteLine(string)
- ret
- }
- }
- .class ChenJD extends People
- {
- .method public void .ctor()
- {
- .maxstack
- ldarg. //1.将实例的引用压栈
- call instance void [mscorlib]System.Object::.ctor() //2.调用基类的构造函数
- ret
- }
- //此处使用newslot属性或者说标签,标识脱离了基类虚函数的那一套链,等同C#中的new
- .method public newslot virtual void Introduce()
- {
- .maxstack
- ldstr "我是陈嘉栋"
- call void [mscorlib]System.Console::WriteLine(string)
- ret
- }
- }
在进行下文之前,匹夫还要先抛出一个概念,哦不,应该是2个概念。
编译时类型和运行时类型
为何要在此提出这2个概念呢?因为这和我们的方法调用息息相关。
举个c#的例子来说明这个问题:
- public abstract class Singer { }
- public class Alin : Singer { } //刚看完我是歌手,喜欢alin...
- class Class1
- {
- public static void Main(string[] args)
- {
- Singer a = new Alin();
- }
- }
对编译器来说,变量的类型就是你声明它时的类型。在此,变量a的类型被定义为Singer。也就是说a的编译时类型是Singer。
但是别急,我们之后又实例化了一个Alin类型的实例,并且将这个实例的引用赋值给了变量a。这就是说,在这段程序运行的时候,编译阶段被定义为Singer类型的变量a所指向的是一块存储了类型Alin的实例的内存。换言之,此时的a的运行时类型是Alin。
那么编译时类型和运行时类型又和我们上面的CIL代码有什么关系呢?下面进入我们的PK阶段~
call vs callvirt
好了,到了这里,我们还是使用CIL代码来实现这个对比。
首先我们自然要声明3个局部变量来分别存储三个类的实例。
其次分别使用call和callvirt来调用方法。不过此处要先和各位看官说明一下,以防一会看的困惑。这里匹夫使用的CIL代码在做目的性很强的演示,所以不要使用日常写C#代码的思路来看下面的对比。此处匹夫首先会实例化3个变量,不过此时这3个变量是作为运行时类型存在的,之后匹夫会手动的使用call或callvirt来调用各个类的方法,所以此处匹夫手动调用的类的类型充当的是编译时类型。
- .method static void Fanyou()
- {
- .entrypoint
- .maxstack
- .locals init (
- class People people,
- class Murong murong,
- class ChenJD chenjd)
- newobj instance void People::.ctor()
- stloc people
- newobj instance void Murong::.ctor()
- stloc murong
- newobj instance void ChenJD::.ctor()
- stloc chenjd
- //Peple
- //编译类型为People,运行时类型为People
- ldloc people
- call instance void People::Introduce()
- //Murong
- //编译类型为Murong,运行时类型为Murong,使用call
- ldloc murong
- call instance void Murong::Introduce()
- //编译类型为People,运行时类型为Murong,使用call
- ldloc murong
- call instance void People::Introduce()
- //编译类型为People,运行时类型为Murong,使用callvirt
- ldloc murong
- callvirt instance void People::Introduce()
- //ChenJD
- //编译类型为ChenJD,运行时类型为ChenJD,使用call
- ldloc chenjd
- callvirt instance void ChenJD::Introduce()
- //编译类型为People,运行时类型为ChenJD,使用call
- ldloc chenjd
- call instance void People::Introduce()
- //编译类型为People,运行时类型为ChenJD,使用callvirt
- ldloc chenjd
- callvirt instance void People::Introduce()
- ret
- }
好了,我们PK的擂台已经搭好了。如果有兴趣的话,各位此时就可以对照各个方法来猜一下输出的结果了。
不过在正式揭晓结局之前,匹夫还是先总结一下这个过程:People类作为基类,有一个虚函数Introduce用来介绍自己。然后Murong类派生自People,同时Murong类也有一个同名的虚函数Introduce,此时可以认为它重载了基类的同名方法。当然好事的匹夫为了对比的更加有趣,又定义了一个派生自People的ChenJD类,同样它也有一个同名的虚函数Introduce,唯一的不同是此时使用了newslot属性。
好啦,此时有了3个分别定义在3个类中的方法。那么问题就来了,我如何正确的让运行时知道我调用的是哪个方法呢?比如编译时类型是People,但是运行时类型却变成了Murong又或者编译时类型是People,但是运行时类型又变成了ChenJD,等等。显然,我想让People的实例去调用定义在People类中的方法,也就是People::Introduce();想让Murong的实例去调用定义在Murong类中的方法,也就是Murong::Introduce();想让ChenJD的实例去调用定义在ChenJD类中方法,也就是ChenJD::Introduce()。
带着这个问题,我们来揭晓上面那场PK的结果。
首先编译,之后运行,最后截图如下:
我们将代码和结果一一对应,可以发现凡是使用call调用方法的:
- call instance void People::Introduce() 输出:我是People,都调用了People中定义的Introduce方法
- call instance void Murong::Introduce() 输出:我是慕容小匹夫,都调用了Murong中定义的Introduce方法
而使用了callvirt来调用方法的:
- callvirt instance void People::Introduce() 输出:我是慕容小匹夫,调用了Murong中重载的Introduce版本。(murong)
- callvirt instance void People::Introduce() 输出:我是People,调用了基类People中原始定义的Introduce。(chenjd)
- callvirt instance void ChenJD::Introduce() 输出:我是陈嘉栋,调用了ChenJD中定义的Introduce。(chenjd)
不知道最后的结果是否和各位之前猜的一致呢?到此,其实我们已经可以得出一些有趣的结论了。那么匹夫就解释一下这个结果吧。
首先,我们聊聊call在这场PK中的表现。
在匹夫的代码中,首先使用call的是
- //编译类型为People,运行时类型为People
- ldloc people
- call instance void People::Introduce()
此时,变量people的引用指向的是一个People的实例,所以调用People的Introduce方法自然而然的输出是“我是People”。
第二处使用call的是
- ldloc murong
- call instance void Murong::Introduce()
- //编译类型为People,运行时类型为Murong,使用call
- ldloc murong
- call instance void People::Introduce()
这两处,变量murong都是Murong类的引用,首先使用call调用Murong::Introduce()方法,输出的是“我是慕容小匹夫”这点自然很好理解。但是之后使用call调用People::Introduce(),输出的却是“我是People”,要注意此时压入栈的变量murong可是一个Murong实例的引用啊。
第三处,也很雷同,变量的运行时类型是ChenJD,编译时类型是People,但是在程序运行时使用call,调用的仍然是编译时类型定义的方法。
可以看出,call对变量的运行时类型根本不感兴趣,而只对编译时类型的方法感兴趣。(当然上一篇文章中匹夫也说过,call还对静态方法感兴趣)。所以此处call只会调用变量编译时类型中定义的方法。
之后,我们再来看看callvirt的表现。
第一处使用callvirt的是
- //编译类型为People,运行时类型为Murong,使用callvirt
- ldloc murong
- callvirt instance void People::Introduce()
此处使用callvirt去调用People::Introduce()方法,但是由于此处变量是murong,它指向的是一个Murong类的实例,因此最后的执行的是Murong类中的重载版本,输出的是“我是慕容小匹夫”。
第二处使用callvirt的是
- //编译类型为ChenJD,运行时类型为ChenJD,使用call
- ldloc chenjd
- callvirt instance void ChenJD::Introduce()
- //编译类型为People,运行时类型为ChenJD,使用callvirt
- ldloc chenjd
- callvirt instance void People::Introduce()
由于ChenJD类中的同名方法使用了newslot属性,所以此处可以看到很明显的对比。使用callvirt去调用People::Introduce()时,执行的并非ChenJD中的Introduce版本,而是基类People中定义的原始Introduce方法。而使用callvirt再去调用ChenJD中的Introduce方法时,执行的自然就是ChenJD中定义的版本了。
这个其实涉及到了虚函数的设计,简单来说可以想象同一系列的虚函数(使用override关键字)存放在一个槽中(slot),在运行时会将没有使用newslot属性的虚函数放入这个槽中,在运行时需要调用虚函数时去这个槽中寻找到符合条件的虚函数执行,而这个槽是谁定义的呢或者说应该如何去定位正确的槽呢?不错,就是通过基类。
如果有兴趣,各位可以虚函数部分的C#代码编译成CIL代码,可以看到调用派生类重载的虚函数,在CIL中其实都是使用callvirt instance xxx baseclass::func 来实现的。
所以,使用了newslot属性的方法并没有放入基类定义的那个槽中,而是自己重新定义了一个新的槽,所以最后callvirt instance void People::Introduce()只能调用基类的原始版本了。
当然,如果有必要匹夫会更具体的写写虚函数的部分,不过现在有点晚了,为了节约时间还是只讨论call和callvirt。
因此,使用callvirt时,它关心的并不是变量定义时的类型是什么,而是变量最后是什么类的引用。也就是说callvirt关心的是变量的运行时类型,是变量真正指向的类型。
假如只有静态函数
看到此时,可能有的看官要抱怨了:匹夫,你说了这么半天怎么好像没有一点关于开篇提到那个本该报错的代码呢?
其实此言差矣,通过分析虚函数,我们发现了call原来只关心变量的编译时类型中定义的函数以及静态函数。如果我们更近一步,就会发现call其实是直接奔着它要调用的那个函数的代码就去了。
直接去执行目标函数中的代码,这样听上去是不是就和类型没有什么关系了呢?
如果,没有所谓的实例函数,只有静态函数,本文开头的问题是不是就有答案了呢?哎,真相也许就是这么简单。
假如所谓的实例函数仅仅是静态函数中传入了一个隐藏的参数“this”,是不是只用静态函数就能实现实例函数了呢?也就是说,当某种(此处我们假设是实例方法)方法把“this”作为参数,但是仍然是一个静态函数,此时使用call去调用它,但是它的参数“this”很不幸的是null,那么这种情况的确没有理由触发NullReferenceException
。
- //注意Foo不是静态方法额~
- public void Foo()
- {
- Console.WriteLine("this == null is " + (this == null));
- }
- //如果它真的是静态函数。。。
- public static void Foo(Murong _this)
- {
- this = _this;
- Console.WriteLine("this == null is " + (this == null));
- }
到此,我们通过分析call 和 callvirt得出的最后一个有趣的结论:实例方法只不过是一个将“this”作为不可见参数的静态方法。
附录:
老规矩,本文的CIL代码如下:
- .assembly extern mscorlib
- {
- .ver :::
- .publickeytoken = (B7 7A 5C E0 ) // .z\V.4..
- }
- .assembly 'HelloWorld'
- {
- }
- .method static void Fanyou()
- {
- .entrypoint
- .maxstack
- .locals init (
- class People people,
- class Murong murong,
- class ChenJD chenjd)
- newobj instance void People::.ctor()
- stloc people
- newobj instance void Murong::.ctor()
- stloc murong
- newobj instance void ChenJD::.ctor()
- stloc chenjd
- //编译类型为People,运行时类型为People
- ldloc people
- call instance void People::Introduce()
- //编译类型为Murong,运行时类型为Murong,使用call
- ldloc murong
- call instance void Murong::Introduce()
- //编译类型为People,运行时类型为Murong,使用call
- ldloc murong
- call instance void People::Introduce()
- //编译类型为People,运行时类型为Murong,使用callvirt
- ldloc murong
- callvirt instance void People::Introduce()
- //编译类型为ChenJD,运行时类型为ChenJD,使用call
- ldloc chenjd
- callvirt instance void ChenJD::Introduce()
- //编译类型为People,运行时类型为ChenJD,使用call
- ldloc chenjd
- call instance void People::Introduce()
- //编译类型为People,运行时类型为ChenJD,使用callvirt
- ldloc chenjd
- callvirt instance void People::Introduce()
- ret
- }
- //如何用CIL声明一个类,请看小匹夫的上一篇文章《用CIL写程序:定义一个叫“慕容小匹夫”的类》
- .class People
- {
- .method public void .ctor()
- {
- .maxstack
- ldarg. //1.将实例的引用压栈
- call instance void [mscorlib]System.Object::.ctor() //2.调用基类的构造函数
- ret
- }
- .method public virtual void Introduce()
- {
- .maxstack
- ldstr "我是People"
- call void [mscorlib]System.Console::WriteLine(string)
- ret
- }
- }
- .class Murong extends People
- {
- .method public void .ctor()
- {
- .maxstack
- ldarg. //1.将实例的引用压栈
- call instance void [mscorlib]System.Object::.ctor() //2.调用基类的构造函数
- ret
- }
- .method public virtual void Introduce()
- {
- .maxstack
- ldstr "我是慕容小匹夫"
- call void [mscorlib]System.Console::WriteLine(string)
- ret
- }
- }
- .class ChenJD extends People
- {
- .method public void .ctor()
- {
- .maxstack
- ldarg. //1.将实例的引用压栈
- call instance void [mscorlib]System.Object::.ctor() //2.调用基类的构造函数
- ret
- }
- //此处使用newslot属性或者说标签,标识脱离了基类虚函数的那一套链接,等同C#中的new
- .method public newslot virtual void Introduce()
- {
- .maxstack
- ldstr "我是陈嘉栋"
- call void [mscorlib]System.Console::WriteLine(string)
- ret
- }
- }
用CIL写程序:从“call vs callvirt”看方法调用的更多相关文章
- 用CIL写程序:写个函数做加法
前言: 上一篇文章小匹夫为CIL正名的篇幅比较多,反而忽略了写那篇文章初衷--即通过写CIL代码来熟悉它,了解它.那么既然有上一篇文章做基础(炮灰),想必各位对CIL的存在也就释然了,兴许也燃起了一点 ...
- 用CIL写程序:定义一个叫“慕容小匹夫”的类
前文回顾: <用CIL写程序:你好,沃尔德> <用CIL写程序:写个函数做加法> 前言: 今天是乙未羊年的第一天,小匹夫先在这里给各位看官拜个年了.不知道各位看官是否和匹夫一样 ...
- 用CIL写程序:你好,沃尔德
前言: 项目紧赶慢赶总算在年前有了一些成绩,所以沉寂了几周之后,小匹夫也终于有时间写点东西了.以前匹夫写过一篇文章,对CIL做了一个简单地介绍,不过不知道各位看官看的是否过瘾,至少小匹夫觉得很不过瘾. ...
- STM32用JLINK 烧写程序时出现NO Cortex-m device found in JTAG chain现象和解决方案
现象 CPU: STM32107VC 用JLINK 烧写程序时出现NO Cortex-m device found in JTAG chain 如图无法查找到硬件就是CPU 提示1:NO Cortex ...
- 第一章-第四题(ACM 比赛的程序是软件么? “写程序” 和 ”做软件“ 有区别么?软件工程是不是教那些不怎么会写程序的人开发软件? 你怎么看?这个游戏团队, 有很好的软件,但是商业模式和其他软件之外的因素呢?有没有考虑到)--By梁旭晖
引用 http://baike.baidu.com/link?url=z_phkcEO4_HjFG_Lt163dGFAubdb68IbfcfzWscTOrrZ55WbJEQKzyMQ5eMQKyatD ...
- 4.“写程序” 这个活动大多数情况下是个人行为。 我们听说的优秀程序员似乎都是单打独斗地完成任务。同学们在大学里也认识一些参加ACM 比赛的编程牛人, 他们写的ACM 比赛的程序是软件么? “写程序” 和 ”做软件“ 有区别么? 请采访这些学生。
ACM的题库的编程都只能算做程序,不能算软件.写程序和做软件区别还是很大的.程序是为实现特定目标或解决特定问题而用计算机语言编写的命令序列的集合.为实现预期目的而进行操作的一系列语句和指令.而软件是程 ...
- IntelliJ下使用Code/Live Template加快编码速度:程序员的工作不是写程序,而是写程序解决问题
程序员的工作不是写程序,而是写程序解决问题. --- 某不知名程序员 我们每天都在写代码,有些代码有结构性的相似,但不是所有的代码都可以被抽成方法.在这种情况下,我们应该考虑使用template的方式 ...
- object-c cocos2d-x 写程序时注意调试的技巧
(1)写程序时最好在类的init函数中显示类名,表明现在在执行哪个类,样例代码 CCLOG(@"cocos2d: Using Director Type:%@", [self cl ...
- 象写程序一样写博客:搭建基于github的博客
象写程序一样写博客:搭建基于github的博客 前言 github 真是无所不能.其 Pages 功能 支持上传 html,并且在页面中显示.于是有好事者做了一个基于 github 的博客管理工具 ...
随机推荐
- 前端CSS预处理器Sass
前面的话 "CSS预处理器"(css preprocessor)的基本思想是,用一种专门的编程语言,进行网页样式设计,然后再编译成正常的CSS文件.SASS是一种CSS的开发工 ...
- [Spring]IoC容器之进击的注解
先啰嗦两句: 第一次在博客园使用markdown编辑,感觉渲染样式差强人意,还是github的样式比较顺眼. 概述 Spring2.5 引入了注解. 于是,一个问题产生了:使用注解方式注入 JavaB ...
- Jquery 搭配 css 使用,简单有效
前几篇博客中讲了Jquery的基础和点击实际,下面来说一下和css搭配着来怎么做 还是和往常一样,举个例子 好几个方块,然后设置颜色 <!DOCTYPE html PUBLIC "-/ ...
- Laravel 5.x 请求的生命周期(附源码)
Laravel最早接触是刚开始实习的时候,那时通过网上的学习资料很快便上手,开发模块接口.后来没有什么深入和总结,但是当我刚开始学Laravel的时候,我对Laravel最大的认识就是,框架除了路由. ...
- Autofac - 属性注入
属性注入不同于通过构造函数方式传入参数. 这里是通过注入的方式, 在类创建完毕之后, 资源释放之前, 给属性赋值. 这里, 我重新弄一些类来演示这一篇吧. public class ClassA { ...
- 三星Note 7停产,原来是吃了流程的亏
三星Note 7发售两个月即成为全球噩梦,从首炸到传言停产仅仅47天.所谓"屋漏偏逢连天雨",相比华为.小米等品牌对其全球市场的挤压.侵蚀,Galaxy Note 7爆炸事件这场连 ...
- DockerCon 2016 – 微软带来了什么?
根据Forrester的调查,接近半数的企业CIO在考虑IT架构的时候更乐于接受开源方案,这主要是基于低成本,避免供应商锁定和敏捷的需求:同时另外一家North Bridge的调研机构的调查显示,20 ...
- [AlwaysOn Availability Groups]健康模型 Part 1——概述
健康模型概述 在成功部署AG之后,跟踪和维护健康状况是很重要的. 1.AG健康模型概述 AG的健康模型是基于策略管理(Policy Based Management PBM)的.如果不熟悉这个特性,可 ...
- Ubuntu设置root用户登录图形界面
Ubuntu默认的是root用户不能登录图形界面的,只能以其他用户登录图形界面.这样就很麻烦,因为权限的问题,不能随意复制删除文件,用gedit编辑文件时经常不能保存,只能用vim去编辑. 解决的办法 ...
- TCP/IP之Nagle算法与40ms延迟
Nagle算法是针对网络上存在的微小分组可能会在广域网上造成拥塞而设计的.该算法要求一个TCP连接上最多只能有一个未被确认的未完成的小分组,在该分组确认到达之前不能发送其他的小分组.同时,TCP收集这 ...