• 关于《你必须知道的.net》第六回的问题IL和C#看似不一致的地方


    本问题源于《你必须知道的.net》第六回,最近在学习anytao的大作《你必须知道的.net》,看到第六回

    深入浅出关键字---base和this时,发现其中有个例子的C#代码和生成的IL似乎不一致。

    1. 问题描述

    主要就是其中base和this示例中的main函数。完整的代码请参考原博客深入浅出关键字---base和this

    public class BaseThisTester
     {
    	 public static void Main(string[] args)
    	 {
    		 Audi audi = new Audi();
    		 audi[1] = "A6";
    		 audi[2] = "A8";
    		 Console.WriteLine(audi[1]);
    		 audi.Run();
    		 audi.ShowResult();
    	 }
     }

    这段代码对应的IL代码如下:

    .method public hidebysig static void  Main(string[] args) cil managed
    {
       .entrypoint
       // 代码大小       61 (0x3d)
       .maxstack  3
       .locals init (class Anytao.net.My_Must_net.Audi V_0)
       IL_0000:  nop
       //使用newobj指令创建新的对象,并调用构造函数初始化
       IL_0001:  newobj     instance void Anytao.net.My_Must_net.Audi::.ctor()
       IL_0006:  stloc.0
       IL_0007:  ldloc.0
       IL_0008:  ldc.i4.1
       IL_0009:  ldstr      "A6"
       IL_000e:  callvirt   instance void Anytao.net.My_Must_net.Vehicle::set_Item(int32,
                                                                                   string)
       IL_0013:  nop
       IL_0014:  ldloc.0
       IL_0015:  ldc.i4.2
       IL_0016:  ldstr      "A8"
       IL_001b:  callvirt   instance void Anytao.net.My_Must_net.Vehicle::set_Item(int32,
                                                                                   string)
       IL_0020:  nop
       IL_0021:  ldloc.0
       IL_0022:  ldc.i4.1
       IL_0023:  callvirt   instance string Anytao.net.My_Must_net.Vehicle::get_Item(int32)
       IL_0028:  call       void [mscorlib]System.Console::WriteLine(string)
       IL_002d:  nop
       IL_002e:  ldloc.0
       IL_002f:  callvirt   instance void Anytao.net.My_Must_net.Vehicle::Run()
       IL_0034:  nop
       IL_0035:  ldloc.0
       //base.ShowResult最终调用的是最高级父类Vehicle的方法,
       //而不是直接父类Car.ShowResult()方法,这是应该关注的
       IL_0036:  callvirt   instance void Anytao.net.My_Must_net.Vehicle::ShowResult()
       IL_003b:  nop
       IL_003c:  ret
    } // end of method BaseThisTester::Main
    

    问题就是最后的一步,也是作者在IL中特意加注释说明的那步 audi.ShowResult();

    这步代码应该是调用Audi这个类的ShowResult()方法,为什么IL中会调用最终的基类Vehicle中的方法呢???

    在这篇博客下面的评论中有些读者已经提出了这个疑问,作者的解释如下:

    而IL分析中关于访问Vehicle::ShowResult的分析,是基于在Audi父类的ShowResult中有base的向上访问,因此最终会追溯到最高级父类,这是原因所在。
    关于多层访问的描述有些欠妥,谢谢讨论,我考虑考虑,及时修订。

    作者解释的原因似乎是由于Audi类的ShowResult()方法中有base.ShowResult(); 所以就一直追朔到了最高级父类。

    如果我们将Audi类的ShowResult()方法中的base.ShowResult(); 注释掉,那么IL中是否还是调用基类Vehicle中的ShowResult()方法呢???

    答案是肯定的,即使注释掉这个代码,IL还是和上面一样,没有任何改变。这也是原博客中评论的第54楼的疑问。

    2. 原因分析

    刚开始看到这个问题的时候,我也是很迷惑,明明Audi类的ShowResult()方法已经override其父类的方法了,为什么IL中还会调用其父类的方法呢?

    后来看了《CLR via C#》这本书,对IL中的这种写法总算有了个合理的解释。至于我的理解对不对,欢各位指教!!!!

    首先CLR中基类和子类的关系如下图:

    捕获

    子类的方法表中不再有父类已经定义的方法了。

    所以本例的三个类的方法表如下:

    捕获

    子类Audi和Car除了构造函数,没有自己定义的新函数。

    同时我们也可以看出 override 只是覆盖父类的方法,不能算是新的方法。

    所以在上面的IL中,调用的是Vehicle类的ShowResult()虚方法,只是在实际运行时JIT根据调用此方法的类型,编译相应的代码。

    本例的调用此方法的类型即为Audi类。

    为了验证上面的想法,我们可以将Audi类中ShowResult()方法的签名改为public new void ShowResult()。

    将原先的override关键字改为new关键字。new关键字表示隐藏父类的同名方法,相当于子类新增了一个方法,与override覆盖基类的方法不同。

    所以改成Audi类中ShowResult()方法的签名改为public new void ShowResult()后,IL中应该调用Audi类中ShowResult()方法。

    下面是修改Audi类后新的Main函数IL代码,与预想的一致。

    .method public static hidebysig 
    	void Main (
    		string[] args
    	) cil managed 
    {
    	// Method begins at RVA 0x216c
    	// Code size 68 (0x44)
    	.maxstack 3
    	.entrypoint
    	.locals init (
    		[0] class Anytao.net.My_Must_net.Audi audi
    	)
    
    	IL_0000: nop
    	IL_0001: newobj instance void Anytao.net.My_Must_net.Audi::.ctor()
    	IL_0006: stloc.0
    	IL_0007: ldloc.0
    	IL_0008: ldc.i4.1
    	IL_0009: ldstr "A6"
    	IL_000e: callvirt instance void Anytao.net.My_Must_net.Vehicle::set_Item(int32, string)
    	IL_0013: nop
    	IL_0014: ldloc.0
    	IL_0015: ldc.i4.2
    	IL_0016: ldstr "A8"
    	IL_001b: callvirt instance void Anytao.net.My_Must_net.Vehicle::set_Item(int32, string)
    	IL_0020: nop
    	IL_0021: ldloc.0
    	IL_0022: ldc.i4.1
    	IL_0023: callvirt instance string Anytao.net.My_Must_net.Vehicle::get_Item(int32)
    	IL_0028: call void [mscorlib]System.Console::WriteLine(string)
    	IL_002d: nop
    	IL_002e: ldloc.0
    	IL_002f: callvirt instance void Anytao.net.My_Must_net.Vehicle::Run()
    	IL_0034: nop
    	IL_0035: ldloc.0
    	IL_0036: callvirt instance void Anytao.net.My_Must_net.Audi::ShowResult()
    	IL_003b: nop
    	IL_003c: ldc.i4.1
    	IL_003d: call valuetype [mscorlib]System.ConsoleKeyInfo [mscorlib]System.Console::ReadKey(bool)
    	IL_0042: pop
    	IL_0043: ret
    } // End of method BaseThisTester.Main

    3. 结论

    通过以上的分析,我觉得IL虽然比c#要更“底层”一些,但还是隐藏了一些CLR的东西。在研究CLR的时候,如果能将IL和C#中看似矛盾的地方都弄清楚,可能能更进一步的理解CLR的原理。也能够对C#语言本身的运行机制有更深刻的理解。

  • 相关阅读:
    testng遇到的一些问题
    Redis-常用命令总结
    Spring AOP
    Spring IOC
    Java-J.U.C总结
    Java-将map拼接成“参数=值&参数=值”
    java多线程-线程池
    mysql 二进制文件增量备份
    Centos下mysql数据库备份与恢复的方法
    CentOS下mysql默认安装位置
  • 原文地址:https://www.cnblogs.com/wang_yb/p/2046886.html
Copyright © 2020-2023  润新知