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

原文地址:https://www.cnblogs.com/wang_yb/p/2046886.html