前文回顾:《用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(256);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 1ldarg.0 //1.将实例的引用压栈call instance void [mscorlib]System.Object::.ctor()  //2.调用基类的构造函数
        ret}  .method public virtual void Introduce(){.maxstack 1ldstr "我是People"call void [mscorlib]System.Console::WriteLine(string)ret}
}.class Murong extends People
{.method public void .ctor(){.maxstack 1ldarg.0 //1.将实例的引用压栈call instance void [mscorlib]System.Object::.ctor()  //2.调用基类的构造函数
        ret}.method public virtual void Introduce(){.maxstack 1ldstr "我是慕容小匹夫"call void [mscorlib]System.Console::WriteLine(string)ret}
}.class ChenJD extends People
{.method public void .ctor(){.maxstack 1ldarg.0 //1.将实例的引用压栈call instance void [mscorlib]System.Object::.ctor()  //2.调用基类的构造函数
        ret}//此处使用newslot属性或者说标签,标识脱离了基类虚函数的那一套链,等同C#中的new.method public newslot virtual void Introduce(){.maxstack 1ldstr "我是陈嘉栋"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 10.locals init (class People    people,class Murong    murong,class ChenJD    chenjd)newobj instance void People::.ctor()stloc peoplenewobj instance void Murong::.ctor()stloc murongnewobj instance void ChenJD::.ctor()stloc chenjd//Peple//编译类型为People,运行时类型为People
    ldloc peoplecall instance void People::Introduce()//Murong//编译类型为Murong,运行时类型为Murong,使用call
    ldloc murongcall instance void Murong::Introduce()//编译类型为People,运行时类型为Murong,使用call
    ldloc murongcall instance void People::Introduce()//编译类型为People,运行时类型为Murong,使用callvirt
    ldloc murongcallvirt instance void People::Introduce()//ChenJD//编译类型为ChenJD,运行时类型为ChenJD,使用call
    ldloc chenjdcallvirt instance void ChenJD::Introduce()//编译类型为People,运行时类型为ChenJD,使用call
    ldloc chenjdcall instance void People::Introduce()//编译类型为People,运行时类型为ChenJD,使用callvirt
    ldloc chenjdcallvirt 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 peoplecall instance void People::Introduce()

此时,变量people的引用指向的是一个People的实例,所以调用People的Introduce方法自然而然的输出是“我是People”。

第二处使用call的是

    ldloc murongcall instance void Murong::Introduce()//编译类型为People,运行时类型为Murong,使用call
    ldloc murongcall 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 murongcallvirt instance void People::Introduce()

此处使用callvirt去调用People::Introduce()方法,但是由于此处变量是murong,它指向的是一个Murong类的实例,因此最后的执行的是Murong类中的重载版本,输出的是“我是慕容小匹夫”。

第二处使用callvirt的是

 //编译类型为ChenJD,运行时类型为ChenJD,使用call
    ldloc chenjdcallvirt instance void ChenJD::Introduce()//编译类型为People,运行时类型为ChenJD,使用callvirt
    ldloc chenjdcallvirt 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 4:0:0:0.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..
}
.assembly 'HelloWorld'
{
}.method static void Fanyou()
{.entrypoint.maxstack 10.locals init (class People    people,class Murong    murong,class ChenJD    chenjd)newobj instance void People::.ctor()stloc peoplenewobj instance void Murong::.ctor()stloc murongnewobj instance void ChenJD::.ctor()stloc chenjd//编译类型为People,运行时类型为People
    ldloc peoplecall instance void People::Introduce()//编译类型为Murong,运行时类型为Murong,使用call
    ldloc murongcall instance void Murong::Introduce()//编译类型为People,运行时类型为Murong,使用call
    ldloc murongcall instance void People::Introduce()//编译类型为People,运行时类型为Murong,使用callvirt
    ldloc murongcallvirt instance void People::Introduce()//编译类型为ChenJD,运行时类型为ChenJD,使用call
    ldloc chenjdcallvirt instance void ChenJD::Introduce()//编译类型为People,运行时类型为ChenJD,使用call
    ldloc chenjdcall instance void People::Introduce()//编译类型为People,运行时类型为ChenJD,使用callvirt
    ldloc chenjdcallvirt instance void People::Introduce()ret
}
//如何用CIL声明一个类,请看小匹夫的上一篇文章《用CIL写程序:定义一个叫“慕容小匹夫”的类》
.class People
{.method public void .ctor(){.maxstack 1ldarg.0 //1.将实例的引用压栈call instance void [mscorlib]System.Object::.ctor()  //2.调用基类的构造函数
        ret}  .method public virtual void Introduce(){.maxstack 1ldstr "我是People"call void [mscorlib]System.Console::WriteLine(string)ret}
}.class Murong extends People
{.method public void .ctor(){.maxstack 1ldarg.0 //1.将实例的引用压栈call instance void [mscorlib]System.Object::.ctor()  //2.调用基类的构造函数
        ret}.method public virtual void Introduce(){.maxstack 1ldstr "我是慕容小匹夫"call void [mscorlib]System.Console::WriteLine(string)ret}
}.class ChenJD extends People
{.method public void .ctor(){.maxstack 1ldarg.0 //1.将实例的引用压栈call instance void [mscorlib]System.Object::.ctor()  //2.调用基类的构造函数
        ret}//此处使用newslot属性或者说标签,标识脱离了基类虚函数的那一套链接,等同C#中的new.method public newslot virtual void Introduce(){.maxstack 1ldstr "我是陈嘉栋"call void [mscorlib]System.Console::WriteLine(string)ret}
}

用CIL写程序:从“call vs callvirt”看方法调用相关推荐

  1. 用CIL写程序:定义一个叫“慕容小匹夫”的类

    前文回顾: <用CIL写程序:你好,沃尔德> <用CIL写程序:写个函数做加法> 前言: 今天是乙未羊年的第一天,小匹夫先在这里给各位看官拜个年了.不知道各位看官是否和匹夫一样 ...

  2. 生信人写程序2. Editplus添加Perl, Shell, R模板和语法高亮

    https://www.editplus.com/ 前言 "工欲善其事必先利其器",生信工程师每天写代码.搭流程,而且要使用至少三门编程语言,没有个好集成开发环境(IDE,Inte ...

  3. stm32使用flymcu烧写程序

    文章目录 一.使用flymcu烧写程序 一.使用flymcu烧写程序 烧写程序之前要使ASP指示灯保持强亮状态,同时要保证使flashIsp模式下也就是灯闪一下模式 如果长按asp按钮指示灯闪烁两次进 ...

  4. 51单片机usb烧录电路_51单片机怎么用usb烧写程序 - 全文

    单片机怎样用usb烧写程序 首先,需要安装keil软件和STC_ISP程序下载软件. 先对你想要实现对单片机的功能用keil编程,然后用STC_ISP下载软件下载到单片机上,最后打开给单片机提供电源就 ...

  5. 如何用python编写一个绘制马赛克图像的自写程序mask = np.zeros

    Python部落(python.freelycode.com)组织翻译,禁止转载,欢迎转发. 这篇教程将会展示如何用python的图形化包"Pygame"和基础的文件I/O来创建一 ...

  6. 写程序是最轻松的事情

    在新的公司工作了也有2个月了. 在这两个月里面有一个非常明显的感觉,那就是---写程序是最轻松的事情.为什么这么说呢? 不需要担心别人会不会配合你的行动,所有的东西都属于自己控制,包括算法.逻辑.实现 ...

  7. 讨论:写程序到底需不需要懂数学?

    数学系所学的数学,跟一般人所会用到的数学不太一样.研究所顺利考上的向往已久的资工所,成为名符其实的本科系学生,本以为可以不用再玩数学了,但我发现我错了,是不用再玩那些抽久的高等数学没错,但线性代数.机 ...

  8. 代码块练习题:看代码写程序的执行结果。

    1 /* 2 代码块练习题: 3 看代码写程序的执行结果. 4 5 输出结果是: 6 林青霞都60了,我很伤心 7 我是main方法 8 Student 静态代码块 9 Student 构造代码块 1 ...

  9. python代码需要背吗-Python 的库、方法这么多,写程序的时候能记住吗?

    能 -- 这就是平时的工作方式.vim写python,没安装其他插件,但有自己的配置文件. 常见的项目所常用的函数很难超过50个.大量的业务函数一旦被写出来就是负责直接处理业务,而不会被其他部分调用, ...

  10. 怎么重写MDK(KEIL)Flash烧写程序

    MDK提供了Flash烧写程序接口,位于文件夹C:\Keil\ARM\Flash (不同的安装目录参考相对路径).KEIL提供了各种的demo,打开_Template文件夹 有个NewDevice的工 ...

最新文章

  1. HTML5:一些部件
  2. Git 好用的客户端 SourceTree破解
  3. Java高并发编程详解系列-Future设计模式
  4. 考研数据结构代码总结
  5. Android图书馆选座系统课程设计
  6. 海湾汉字编码表全部_汉字编码简明对照表
  7. adb 驱动安装说明文档
  8. 倪捷:智能语音扩展数字化服务
  9. 量子计算机模拟黑洞纠缠,科学家想用量子纠缠探查黑洞内部?那得先找到自旋方向相反的光子...
  10. 项目实操总结:拼团活动的设计
  11. 【Python算法】:n个点m条边有权无向图
  12. Python——提取复数类型的数组的的实数部分和虚数部分
  13. Shader效果实现:双色渐变
  14. RGB 到HSV转换 摘自wiki百科
  15. 描述性统计部分(一)----统计量
  16. 两周年无人问津,EOS到底做错了什么
  17. python感叹号是什么意思_监控画面的这个感叹号是什么意思
  18. 设置鼠标连接时触摸板禁用、Win10系统电脑触摸板使用方法总结
  19. Esri与欧盟委员会签订许可协议
  20. display:none 和 visibility:hidden的区别

热门文章

  1. 运动控制 编码器记录
  2. 数学建模算法与应用(目录)
  3. cad小插件文字刷_必备CAD插件大全,内含最全字体库
  4. 高等代数——大学高等代数课程创新教材(丘维声)——1.3笔记+习题
  5. python读写pdf_Python读写PDF
  6. 计算机系统建模与仿真论文,《系统建模与仿真》课程论文.pdf
  7. MATLAB 50行代码绘制超好看的旋转九边形
  8. 线性规划与整数规划小结
  9. 【源码】迭代法求根的matlab算法
  10. 网站扫描服务器全部开放端口,服务器开放端口扫描