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

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

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

1. 问题描述

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

1
2
3
4
5
6
7
8
9
10
11
12
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代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
.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代码,与预想的一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
.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#语言本身的运行机制有更深刻的理解。

posted @   wang_yb  阅读(773)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
点击右上角即可分享
微信分享提示