• .NET中继承和多态深入剖析(下)


    class Program  
    2     {  
    3         staticvoid Main(string[] args)  
    4 {
    5             Cpu c1 =new Cpu();  
    6             c1.fun();  
    7             Cpu c2 =new IntelCpu();  
    8             c2.fun();  
    9             Cpu c3_1 =new NewCpu();  
    10             c3_1.fun();  
    11             NewCpu c3_2 =new NewCpu();  
    12             c3_2.fun();  
    13             Console.ReadKey();  
    14         }  
    15     }  
    16     class Cpu  
    17     {  
    18         publicvirtualvoid fun()  
    19         {  
    20             Console.WriteLine("This is Cpu");  
    21         }  
    22     }  
    23     class IntelCpu : Cpu  
    24     {  
    25         publicoverridevoid fun()  
    26         {  
    27             Console.WriteLine("This is IntelCpu");  
    28         }  
    29     }  
    30     class NewCpu : Cpu  
    31     {  
    32         publicnewvoid fun()  
    33         {  
    34             Console.WriteLine("This is NewCpu");  
    35         }  

     

     

    这个例子,主要是为了解释上一篇中遗留的new关键字时候的问题。我们主要从两个方面来讲解这个列子:

    1:变量类型同对象类型不同的场合
    C2和C3_1这两个对象的变量类型都是CPU类型,分配在栈上:

    在编译前:他们都只能使用变量类型所具有的方法,也就是CPU::FUN() 方法。

    在编译时:编译器发现CPU::FUN()是一个虚方法,然后便获得了FUN的方法槽偏移量为【28H】 ,C2和C3_1对象调用Fun()方法的地址是【对象方法表地址】+【28H】 ;

    在运行时:因为虚方法是通过callvirt 指令调用 ,需要知道具体的对象类型,这个时候C2对象是IntelCpu类型,而C3_1对象是NewCpu类型。于是访问C2.FUN()时地址是【IntelCpu类型地址】+【28H】;而C3_1.FUN()的地址是【NewCpu】+【28H】

     

    上面就是编译后的内存方法表的布局情况。IntelCpu类型使用了override关键字,所以方法槽偏移量为【28H】不再指向CPU对象的方法地址,而NewCpu类型对象使用了new关键字,所以继承的方法槽偏移量为【28H】的地址仍旧指向CPU对象的方法地址,只是在下一个方法槽创建了一个新的fun方法。更具上面的图就很清楚的看出了运行时的结果。

    2 变量类型同对象类型相同的场合
    C1和C3_2这2个对象的变量类型与类型对象是相同的。

    在编译前:C1变量是Cpu类型,所以能见的是CPU::FUN()方法;而C3_2变量是NewCpu类型,能见的是NewCpu::Fun()方法(因为用New关键字覆盖了)

    在编译时:发现C1的fun方法是虚方法,所以才C1.fun访问地址是【对象方法表地址】+【28H】 ;而C3_2的fun方法不是虚方法,所以编译器可以直接确定此方法的地址【0x0001】。

    在运行时:同样是使用callvirt 指令调用,因为他们变量类型与类型对象是相同的,所以不会表现出多态。

     

    上面就是这种情况的方法表布局。可以看到NewCpu类型对象有两个fun方法,一个是继承于Cpu类型对象的虚方法,一个是自己新建的方法。C3_1对象同C3_2对象区别就在于他们栈上的变量类型不同。C3_1在编译时是Cpu对象,可见的fun是虚方法,所以获得了继承的Fun方法的方法槽偏移量,而C3_2在编译时NewCpu对象,可见的fun方法是非虚方法,所以直接得到了自己的fun方法的地址。

    另外要补充的就是对于new override的情况,这个是和new一样的,不同的只是自己新建的这个方法是一个虚方法。而如果直接使用override方法,被重写的方法仍旧是虚方法,可以被自己的子类继续重写,一层一层。

    二 更进一步的例子
    对于override和new的情况,应该说是应该比较清楚了,那么接着看下面的列子吧。

     

    代码

     

     

    上面的列子中在父类中有2个虚方法,一个非虚方法。而子类中,只是覆盖了一个虚方法。而我们要关注的也就是这个子类的调用情况。因为父类没有任何方法被重写,所以准确的说,这里并不能算是一个多态的例子。但是有了虚方法,有了new,总是容易和多态混淆。还是那句话,弄清楚了方法表布局,一切都不在是问题。

     

    再次提醒一次,NewCpu方法表只会继承基类的虚方法到自己的方法槽表中,并且保持相同的布局; 所以此时的内存方法表应该如上图所示。fun1和fun3两个虚方法继承于父类,并且保持了相同的布局。而fun2是非虚方法,所以没有被继承。

    调用fun1方法,和前一个列子是相同的,因为被覆盖,并且是非虚方法,所以编译时确定了地址,

    调用fun2方法,因为fun2是父类的一个非虚方法,所以也是编译时确定了地址

    调用fun3方法,因为fun3方法是父类的一个虚方法,所以编译时只能确定方法槽的偏移量,而要在运行时确定运行地址。

    + expand sourceview plaincopy to clipboardprint?
    ·········10········20········30········40········50········60········70········80········90········100·······110·······120·······130·······140·······150
               NewCpu c1 = new NewCpu();   
    00000035  mov         ecx,0AB311Ch    
    0000003a  call        FFB41FAC    
    0000003f  mov         dword ptr [ebp-44h],eax    
    00000042  mov         ecx,dword ptr [ebp-44h]    
    00000045  call        FFB5C000    
    0000004a  mov         eax,dword ptr [ebp-44h]    
    0000004d  mov         dword ptr [ebp-40h],eax    
                c1.fun1();   
    00000050  mov         ecx,dword ptr [ebp-40h]    
    00000053  cmp         dword ptr [ecx],ecx    
    00000055  call        FFB5BFF8    
    0000005a  nop                 
                c1.fun2();   
    0000005b  mov         ecx,dword ptr [ebp-40h]    
    0000005e  cmp         dword ptr [ecx],ecx    
    00000060  call        FFB5BFC8    
    00000065  nop                 
                c1.fun3();   
    00000066  mov         ecx,dword ptr [ebp-40h]    
    00000069  mov         eax,dword ptr [ecx]    
    0000006b  call        dword ptr [eax+3Ch]    
    0000006e  nop                
               NewCpu c1 = new NewCpu();
    00000035  mov         ecx,0AB311Ch 
    0000003a  call        FFB41FAC 
    0000003f  mov         dword ptr [ebp-44h],eax 
    00000042  mov         ecx,dword ptr [ebp-44h] 
    00000045  call        FFB5C000 
    0000004a  mov         eax,dword ptr [ebp-44h] 
    0000004d  mov         dword ptr [ebp-40h],eax 
                c1.fun1();
    00000050  mov         ecx,dword ptr [ebp-40h] 
    00000053  cmp         dword ptr [ecx],ecx 
    00000055  call        FFB5BFF8 
    0000005a  nop              
                c1.fun2();
    0000005b  mov         ecx,dword ptr [ebp-40h] 
    0000005e  cmp         dword ptr [ecx],ecx 
    00000060  call        FFB5BFC8 
    00000065  nop              
                c1.fun3();
    00000066  mov         ecx,dword ptr [ebp-40h] 
    00000069  mov         eax,dword ptr [ecx] 
    0000006b  call        dword ptr [eax+3Ch] 
    0000006e  nop              

    上面是编译后的汇编代码,大家也可以到call指令后的地址形式。只有fun3是间接寻址,而其他是直接寻址。这里唯一的问题就是对fun2方法的调用。fun2没有被继承下来,那么NewCpu对象是如何去Cpu对象中得到他的地址的呢?

    view plaincopy to clipboardprint?
    TypeDef #4 (02000005)   
    -------------------------------------------------------   
        TypDefName: virtual.NewCpu  (02000005)   
        Flags     : [NotPublic] [AutoLayout] [Class] [AnsiClass] [BeforeFieldInit]  (00100000)   
        Extends   : 02000004 [TypeDef] virtual.Cpu   
        Method #1 (06000009)    
        -------------------------------------------------------   
            MethodName: fun1 (06000009)   
            Flags     : [Public] [HideBySig] [ReuseSlot]  (00000086)   
            RVA       : 0x000020e8   
            ImplFlags : [IL] [Managed]  (00000000)   
            CallCnvntn: [DEFAULT]   
            hasThis    
            ReturnType: Void   
            No arguments.   
            Signature : 20 00 01   
    TypeDef #4 (02000005)
    -------------------------------------------------------
     TypDefName: virtual.NewCpu  (02000005)
     Flags     : [NotPublic] [AutoLayout] [Class] [AnsiClass] [BeforeFieldInit]  (00100000)
     Extends   : 02000004 [TypeDef] virtual.Cpu
     Method #1 (06000009) 
     -------------------------------------------------------
      MethodName: fun1 (06000009)
      Flags     : [Public] [HideBySig] [ReuseSlot]  (00000086)
      RVA       : 0x000020e8
      ImplFlags : [IL] [Managed]  (00000000)
      CallCnvntn: [DEFAULT]
      hasThis 
      ReturnType: Void
      No arguments.
      Signature : 20 00 01 

    上面是NewCpu生成的IL代码,我们可以发现Extends项目中只是了它的父类是Cpu类,这样在编译时,虽然在自身类中找不到fun2方法,但是系统会去他的父类中找到此方法并确定方法的地址,而在我们编译前,智能感知中能找到fun2,也是依靠元数据来实现的。而在子类实例化之前,调用父类构造函数,应该也是同一个道理。

    三 终极武器
    光凭空YY,是解决不了问题的,这个时候我就要要用sos.dll来调试代码; 看看内存布局到底是个啥样子。

    为了看的清楚,在上面代码中加入了Cpu的对象c,来分别调用3个方法:

    1:查看实际的内存方法表布局

    view plaincopy to clipboardprint?
    .load sos   
    已加载扩展 C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\sos.dll   
    !clrstack -a   //查看堆栈信息   
    PDB symbol for mscorwks.dll not loaded   
    OS Thread Id: 0xbf8 (3064)   
    ESP       EIP        
    0013f418 00f6011c virtual.Program.Main(System.String[])   
        PARAMETERS:   
            args = 0x01432f58   
        LOCALS:  //我们堆栈上的两个对象    
            0x0013f430 = 0x01432f68   // 对象c   
            0x0013f42c = 0x01452184   // 对象c1   
    0013f68c 79e71b4c [GCFrame: 0013f68c]    
    //查看对象c的类型对象   
    !dumpobj 0x01432f68   
    Name: virtual.Cpu   
    MethodTable: 00ab30a4   
    EEClass: 00ab1368   
    Size: 12(0xc) bytes   
     (E:\program\CSharp\Practice\virtual\virtual\bin\Debug\virtual.exe)   
    Fields:   
    None   
    // 查看CPU类型的方法表   
    !dumpmt -md 00ab30a4      
    EEClass: 00ab1368   
    Module: 00ab2c5c   
    Name: virtual.Cpu   
    mdToken: 02000003  (E:\program\CSharp\Practice\virtual\virtual\bin\Debug\virtual.exe)   
    BaseSize: 0xc   
    ComponentSize: 0x0   
    Number of IFaces in IFaceMap: 0   
    Slots in VTable: 8   
    --------------------------------------   
    MethodDesc Table  // 这里就是她的方法表了   
       Entry MethodDesc      JIT Name   
    035f6aa0   03474924   PreJIT System.Object.ToString()   
    035f6ac0   0347492c   PreJIT System.Object.Equals(System.Object)   
    035f6b30   0347495c   PreJIT System.Object.GetHashCode()   
    03667410   03474980   PreJIT System.Object.Finalize()   
    00abc030   00ab3070      JIT virtual.Cpu.fun1()   
    00abc040   00ab308c      JIT virtual.Cpu.fun3()   
    00abc048   00ab3098      JIT virtual.Cpu..ctor()   
    00abc038   00ab307c      JIT virtual.Cpu.fun2()  
    .load sos
    已加载扩展 C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\sos.dll
    !clrstack -a   //查看堆栈信息
    PDB symbol for mscorwks.dll not loaded
    OS Thread Id: 0xbf8 (3064)
    ESP       EIP     
    0013f418 00f6011c virtual.Program.Main(System.String[])
        PARAMETERS:
            args = 0x01432f58
        LOCALS:  //我们堆栈上的两个对象 
            0x0013f430 = 0x01432f68   // 对象c
            0x0013f42c = 0x01452184   // 对象c1
    0013f68c 79e71b4c [GCFrame: 0013f68c] 
    //查看对象c的类型对象
    !dumpobj 0x01432f68
    Name: virtual.Cpu
    MethodTable: 00ab30a4
    EEClass: 00ab1368
    Size: 12(0xc) bytes
     (E:\program\CSharp\Practice\virtual\virtual\bin\Debug\virtual.exe)
    Fields:
    None
    // 查看CPU类型的方法表
    !dumpmt -md 00ab30a4   
    EEClass: 00ab1368
    Module: 00ab2c5c
    Name: virtual.Cpu
    mdToken: 02000003  (E:\program\CSharp\Practice\virtual\virtual\bin\Debug\virtual.exe)
    BaseSize: 0xc
    ComponentSize: 0x0
    Number of IFaces in IFaceMap: 0
    Slots in VTable: 8
    --------------------------------------
    MethodDesc Table  // 这里就是她的方法表了
       Entry MethodDesc      JIT Name
    035f6aa0   03474924   PreJIT System.Object.ToString()
    035f6ac0   0347492c   PreJIT System.Object.Equals(System.Object)
    035f6b30   0347495c   PreJIT System.Object.GetHashCode()
    03667410   03474980   PreJIT System.Object.Finalize()
    00abc030   00ab3070      JIT virtual.Cpu.fun1()
    00abc040   00ab308c      JIT virtual.Cpu.fun3()
    00abc048   00ab3098      JIT virtual.Cpu..ctor()
    00abc038   00ab307c      JIT virtual.Cpu.fun2()

    从上面我们清楚的看到Cpu类型对象的方法表中有我们自己定义的3个方法,Entry就是方法槽偏移量,也是递增的。接下来看看NewCpu对象的方法表:

    !dumpmt -md 00ab311c   
    EEClass: 00ab13cc   
    Module: 00ab2c5c   
    Name: virtual.NewCpu   
    mdToken: 02000004  (E:\program\CSharp\Practice\virtual\virtual\bin\Debug\virtual.exe)   
    BaseSize: 0xc   
    ComponentSize: 0x0   
    Number of IFaces in IFaceMap: 0   
    Slots in VTable: 8   
    --------------------------------------   
    MethodDesc Table   
       Entry MethodDesc      JIT Name   
    035f6aa0   03474924   PreJIT System.Object.ToString()   
    035f6ac0   0347492c   PreJIT System.Object.Equals(System.Object)   
    035f6b30   0347495c   PreJIT System.Object.GetHashCode()   
    03667410   03474980   PreJIT System.Object.Finalize()   
    00abc030   00ab3070      JIT virtual.Cpu.fun1()   
    00abc040   00ab308c      JIT virtual.Cpu.fun3()   
    00abc070   00ab3110      JIT virtual.NewCpu..ctor()   
    00abc068   00ab3100      JIT virtual.NewCpu.fun1()  
    !dumpmt -md 00ab311c
    EEClass: 00ab13cc
    Module: 00ab2c5c
    Name: virtual.NewCpu
    mdToken: 02000004  (E:\program\CSharp\Practice\virtual\virtual\bin\Debug\virtual.exe)
    BaseSize: 0xc
    ComponentSize: 0x0
    Number of IFaces in IFaceMap: 0
    Slots in VTable: 8
    --------------------------------------
    MethodDesc Table
       Entry MethodDesc      JIT Name
    035f6aa0   03474924   PreJIT System.Object.ToString()
    035f6ac0   0347492c   PreJIT System.Object.Equals(System.Object)
    035f6b30   0347495c   PreJIT System.Object.GetHashCode()
    03667410   03474980   PreJIT System.Object.Finalize()
    00abc030   00ab3070      JIT virtual.Cpu.fun1()
    00abc040   00ab308c      JIT virtual.Cpu.fun3()
    00abc070   00ab3110      JIT virtual.NewCpu..ctor()
    00abc068   00ab3100      JIT virtual.NewCpu.fun1()

    如何,看明白了吧,继承下来的fun1和fun3的地址是一样的,而方法表中确实没有fun2的身影。平时我们说的继承,子类会继承父类的所有方法(包括私有方法,只是不能访问,但不包括构造方法),这个实际是逻辑上的继承,而真正物理上的集成只对虚方法有效。而且对于非虚方法也不需要去继承到子类中,因为这就是代码重用吗。哈哈!

    2:MethodDesc
    在看看上面的MethodDesc这个字段,方法描述(MethodDesc)是CLR知道的方法实现的一个封装。方法描述在类加载过程中产生,初始化为指向IL。每个方法描述带有一个预编译代理(PreJitStub),负责触发JIT编译。下图显示了一个典型的布局,方法表的槽实际上指向代理,而不是实际的方法描述数据结构。对于实际的方法描述,这是-5字节的偏移,是每个方法的8个附加字节的一部分。这5个字节包含了调用预编译代理程序的指令。5字节的偏移可以从SOS的DumpMT输出从看到,因为方法描述总是方法槽表指向的位置后面的5个字节。在第一次调用时,会调用JIT编译程序。在编译完成后,包含调用指令的5个字节会被跳转到JIT编译后的x86代码的无条件跳转指令覆盖。(转)

     

    3:EEClass

    EEClass在方法表创建前开始生存,它和方法表结合起来,是类型声明的CLR版本。实际上,EEClass和方法表逻辑上是一个数据结构(它们一起表示一个类型),只不过因为使用频度的不同而被分开。经常使用的域放在方法表,而不经常使用的域在EEClass中。这样,需要被JIT编译函数使用的信息(如名字,域和偏移)在EEClass中,但是运行时需要的信息(如虚表槽和GC信息)在方法表中。

    对每一个类型会加载一个EEClass到应用程序域中,包括接口,类,抽象类,数组和结构。每个EEClass是一个被执行引擎跟踪的树的节点。CLR使用这个网络在EEClass结构中浏览,其目的包括类加载,方法表布局,类型验证和类型转换。EEClass的子-父关系基于继承层次建立,而父-子关系基于接口层次和类加载顺序的结合。在执行托管代码的过程中,新的EEClass节点被加入,节点的关系被补充,新的关系被建立。在网络中,相邻的EEClass还有一个水平的关系。EEClass有三个域用于管理被加载类型的节点关系:父类(Parent Class),相邻链(sibling chain)和子链(children chain)。

     

    我们通过使用!DumpClass可以查看EEClass

    !DumpClass 00ab13cc   
    Class Name: virtual.NewCpu   
    mdToken: 02000004 (E:\program\CSharp\Practice\virtual\virtual\bin\Debug\virtual.exe)   
    Parent Class: 00ab1368   
    Module: 00ab2c5c   
    Method Table: 00ab311c   
    Vtable Slots: 6   
    Total Method Slots: 7   
    Class Attributes: 100000     
    NumInstanceFields: 0   
    NumStaticFields: 0   
    !DumpClass 00ab1368   
    Class Name: virtual.Cpu   
    mdToken: 02000003 (E:\program\CSharp\Practice\virtual\virtual\bin\Debug\virtual.exe)   
    Parent Class: 03433ef0   
    Module: 00ab2c5c   
    Method Table: 00ab30a4   
    Vtable Slots: 6   
    Total Method Slots: 7   
    Class Attributes: 100000     
    NumInstanceFields: 0   
    NumStaticFields: 0   
    !DumpClass 03433ef0   
    Class Name: System.Object   
    mdToken: 02000002 (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)   
    Parent Class: 00000000   
    Module: 03431000   
    Method Table: 036a061c   
    Vtable Slots: 4   
    Total Method Slots: a   
    Class Attributes: 102001     
    NumInstanceFields: 0   
    NumStaticFields: 0  
    !DumpClass 00ab13cc
    Class Name: virtual.NewCpu
    mdToken: 02000004 (E:\program\CSharp\Practice\virtual\virtual\bin\Debug\virtual.exe)
    Parent Class: 00ab1368
    Module: 00ab2c5c
    Method Table: 00ab311c
    Vtable Slots: 6
    Total Method Slots: 7
    Class Attributes: 100000  
    NumInstanceFields: 0
    NumStaticFields: 0
    !DumpClass 00ab1368
    Class Name: virtual.Cpu
    mdToken: 02000003 (E:\program\CSharp\Practice\virtual\virtual\bin\Debug\virtual.exe)
    Parent Class: 03433ef0
    Module: 00ab2c5c
    Method Table: 00ab30a4
    Vtable Slots: 6
    Total Method Slots: 7
    Class Attributes: 100000  
    NumInstanceFields: 0
    NumStaticFields: 0
    !DumpClass 03433ef0
    Class Name: System.Object
    mdToken: 02000002 (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Parent Class: 00000000
    Module: 03431000
    Method Table: 036a061c
    Vtable Slots: 4
    Total Method Slots: a
    Class Attributes: 102001  
    NumInstanceFields: 0
    NumStaticFields: 0
     

    上面的代码中我们从NewCpu对象开始查看,可以看到她Parent Class的地址,这个就是当前父对象的类型EEClass地址。我们继续看Cpu对象的父对象发现是Object,而她的父对象地址是 00000000。而这个结构中,还包括了类中定义的静态字段和实例字段数;Vtable Slots是虚方法数量和实现的接口方法,而父类的借口方法也会被继承下来,因为接口方法也是虚方法,虽然被继承但不能被重写,因为IL代码中有final关键字 ,而Total Method  Slots是类中总的的方法。由此可见,在NewCpu方法中,虚方法有6个,4个继承与Object,2个继承与Cpu;而总的方法有7个,6个虚方法,和一个继承与Cup的非虚方法。

    关于使用sos调试的命令可以参见:http://msdn.microsoft.com/zh-cn/library/bb190764.aspx

    四 抽象方法、虚方法和接口方法

    class Program
    2{
    3staticvoid Main(string[] args)
    4{
    5Console.ReadKey();
    6}
    7}
    8interface ITest
    9{
    10void test();
    11}
    12abstractclass BaseCpu
    13{
    14publicabstractvoid fun1();
    15publicvirtualvoid fun2()
    16{
    17Console.WriteLine("This is BaseCpu fun2");
    18}
    19}
    20class Cpu : BaseCpu , ITest
    21{
    22publicoverridevoid fun1()
    23{
    24Console.WriteLine("This is Cpu fun1");
    25}
    26publicoverridevoid fun2()
    27{
    28Console.WriteLine("This is Cpu fun2");
    29}
    30publicvoid test()
    31{
    32thrownew Exception("The method or operation is not implemented.");
    33}
    34}
    35class NewCpu : Cpu
    36{
    37publicoverridevoid fun1()
    38{
    39Console.WriteLine("This is NewCpu fun1");
    40}
    41publicoverridevoid fun2()
    42{
    43Console.WriteLine("This is NewCpu fun2");
    44}
    45}
    46class Program
    47{
    48staticvoid Main(string[] args)
    49{
    50Console.ReadKey();
    51}
    52}
    53interface ITest
    54{
    55void test();
    56}
    57abstractclass BaseCpu
    58{
    59publicabstractvoid fun1();
    60publicvirtualvoid fun2()
    61{
    62Console.WriteLine("This is BaseCpu fun2");
    63}
    64}
    65class Cpu : BaseCpu , ITest
    66{
    67publicoverridevoid fun1()
    68{
    69Console.WriteLine("This is Cpu fun1");
    70}
    71publicoverridevoid fun2()
    72{
    73Console.WriteLine("This is Cpu fun2");
    74}
    75publicvoid test()
    76{
    77thrownew Exception("The method or operation is not implemented.");
    78}
    79}
    80class NewCpu : Cpu
    81{
    82publicoverridevoid fun1()
    83{
    84Console.WriteLine("This is NewCpu fun1");
    85}
    86publicoverridevoid fun2()
    87{
    88Console.WriteLine("This is NewCpu fun2");
    89}
    90}
    91   

     

    上面的列子包含了抽象方法,虚方法和接口方法,以及他们的继承和重写。实际上抽象方法和接口方法都是虚方法,只不过他们不需要也不能显示的使用virtual关键字。我们通过ILDASM来查看他们的IL有什么区别。

    view plaincopy to clipboardprint?
    .method public hidebysig newslot abstract virtual    
            instance void  fun1() cil managed   
    {   
    } // end of method BaseCpu::fun1   
    .method public hidebysig newslot virtual    
            instance void  fun2() cil managed   
    {   
      // 代码大小       13 (0xd)   
      .maxstack  8   
      IL_0000:  nop   
      IL_0001:  ldstr      "This is BaseCpu fun2"  
      IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)   
      IL_000b:  nop   
      IL_000c:  ret   
    } // end of method BaseCpu::fun2   
    .method public hidebysig newslot abstract virtual    
            instance void  test() cil managed   
    {   
    } // end of method ITest::test   
    .method public hidebysig newslot abstract virtual 
            instance void  fun1() cil managed
    {
    } // end of method BaseCpu::fun1
    .method public hidebysig newslot virtual 
            instance void  fun2() cil managed
    {
      // 代码大小       13 (0xd)
      .maxstack  8
      IL_0000:  nop
      IL_0001:  ldstr      "This is BaseCpu fun2"
      IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
      IL_000b:  nop
      IL_000c:  ret
    } // end of method BaseCpu::fun2
    .method public hidebysig newslot abstract virtual 
            instance void  test() cil managed
    {
    } // end of method ITest::test 

    可以看到3种方法的IL代码都有virtual关键字,说明他们全是虚方法。不同的是接口和抽象方法都有abstract方法,表示他们都是抽象的,所以非抽象类或非接口继承他们之后都需要被实现。

    我们接着看继承他们的类的IL代码

    view plaincopy to clipboardprint?
    .method public hidebysig virtual instance void    
            fun1() cil managed   
    {   
      // 代码大小       13 (0xd)   
      .maxstack  8   
      IL_0000:  nop   
      IL_0001:  ldstr      "This is Cpu fun1"  
      IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)   
      IL_000b:  nop   
      IL_000c:  ret   
    } // end of method Cpu::fun1   
    .method public hidebysig virtual instance void    
            fun2() cil managed   
    {   
      // 代码大小       13 (0xd)   
      .maxstack  8   
      IL_0000:  nop   
      IL_0001:  ldstr      "This is Cpu fun2"  
      IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)   
      IL_000b:  nop   
      IL_000c:  ret   
    } // end of method Cpu::fun2   
    .method public hidebysig newslot virtual final    
            instance void  test() cil managed   
    {   
      // 代码大小       12 (0xc)   
      .maxstack  8   
      IL_0000:  nop   
      IL_0001:  ldstr      "The method or operation is not implemented."  
      IL_0006:  newobj     instance void [mscorlib]System.Exception::.ctor(string)   
      IL_000b:  throw  
    } // end of method Cpu::test  
    .method public hidebysig virtual instance void 
            fun1() cil managed
    {
      // 代码大小       13 (0xd)
      .maxstack  8
      IL_0000:  nop
      IL_0001:  ldstr      "This is Cpu fun1"
      IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
      IL_000b:  nop
      IL_000c:  ret
    } // end of method Cpu::fun1
    .method public hidebysig virtual instance void 
            fun2() cil managed
    {
      // 代码大小       13 (0xd)
      .maxstack  8
      IL_0000:  nop
      IL_0001:  ldstr      "This is Cpu fun2"
      IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
      IL_000b:  nop
      IL_000c:  ret
    } // end of method Cpu::fun2
    .method public hidebysig newslot virtual final 
            instance void  test() cil managed
    {
      // 代码大小       12 (0xc)
      .maxstack  8
      IL_0000:  nop
      IL_0001:  ldstr      "The method or operation is not implemented."
      IL_0006:  newobj     instance void [mscorlib]System.Exception::.ctor(string)
      IL_000b:  throw
    } // end of method Cpu::test
     

    上面的Cpu类分别重写了3种方法。抽象方法和虚方法是相同的,而接口却多了一个final关键字,这样的话,此接口方法不能被子类重写。虽然他是虚方法。如果需要接口方法能被重写,需要显示的加上Virtual关键字。而如果希望一个虚方法不能被不能被子类重写,那么可以使用sealed关键字,而不能使用private来限制虚方法。 效果如下IL代码:

    view plaincopy to clipboardprint?
    //让接口方法可被重写,使用virtual关键字   
    public virtual void test()   
    {   
      throw new Exception("The method or operation is not implemented.");   
    }   
    .method public hidebysig newslot virtual    
            instance void  test() cil managed   
    {   
      // 代码大小       12 (0xc)   
      .maxstack  8   
      IL_0000:  nop   
      IL_0001:  ldstr      "The method or operation is not implemented."  
      IL_0006:  newobj     instance void [mscorlib]System.Exception::.ctor(string)   
      IL_000b:  throw  
    } // end of method Cpu::test   
    //---------------------------------------------------   
    //让虚方法不能被重写,使用sealed 关键字   
    public sealed override void fun2()   
    {   
       Console.WriteLine("This is Cpu fun2");   
    }   
    .method public hidebysig virtual final instance void    
            fun2() cil managed   
    {   
      // 代码大小       13 (0xd)   
      .maxstack  8   
      IL_0000:  nop   
      IL_0001:  ldstr      "This is Cpu fun2"  
      IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)   
      IL_000b:  nop   
      IL_000c:  ret   
    } // end of method Cpu::fun2  
    //让接口方法可被重写,使用virtual关键字
    public virtual void test()
    {
      throw new Exception("The method or operation is not implemented.");
    }
    .method public hidebysig newslot virtual 
            instance void  test() cil managed
    {
      // 代码大小       12 (0xc)
      .maxstack  8
      IL_0000:  nop
      IL_0001:  ldstr      "The method or operation is not implemented."
      IL_0006:  newobj     instance void [mscorlib]System.Exception::.ctor(string)
      IL_000b:  throw
    } // end of method Cpu::test
    //---------------------------------------------------
    //让虚方法不能被重写,使用sealed 关键字
    public sealed override void fun2()
    {
       Console.WriteLine("This is Cpu fun2");
    }
    .method public hidebysig virtual final instance void 
            fun2() cil managed
    {
      // 代码大小       13 (0xd)
      .maxstack  8
      IL_0000:  nop
      IL_0001:  ldstr      "This is Cpu fun2"
      IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
      IL_000b:  nop
      IL_000c:  ret
    } // end of method Cpu::fun2

    有意思的是,如果你吧虚方法定义为private,在编码时,只能感知会更具元数据来显示出这个方法为可重写的方法,但是编译时会报错,所以不知道这算不算一个小BUG

  • 相关阅读:
    诸暨集训游记
    P2678 跳石头
    P1577 切绳子
    P1328 生活大爆炸版石头剪刀布
    P1067 多项式输出
    分解因数
    【管理篇】团队组织与架构演进方法论
    【状态机】行为状体机和协议状态机
    【数据库】分库分表
    【OLAP】从数仓到Kappa架构
  • 原文地址:https://www.cnblogs.com/qianyz/p/2235238.html
Copyright © 2020-2023  润新知