关于《你必须知道的.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#语言本身的运行机制有更深刻的理解。