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

