.NET_STAR

打造技术团队,愿与您共同开创事业!

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: :: 管理 ::
金老师:
    您好!
    我是一名.net程序员。拜读了您的“.net 2.0面向对象编程揭秘”这本书,受益匪浅。
    您的书写得非常深入,经常让我有恍然大悟的感觉。很多堆积已久的问题迎刃而解。同时我也有了很多疑问。希望您能帮我解答。
 
    1.您说子类会调用父类的构造函数。同时“子类对象集成了基类的实例字段”(P213)。“基类的实例字段”包括父类的private字段么?
    
    2.Son son=new Son();
      Father father=son;
      这是您介绍的多态编程。子类实例变量赋值给父类变量。
      这样的两句代码,在内存中将会发生什么呢?
      对象的实例没有改变么?faher和son存的是同一个首地址?
      这时father能调用的都是Father类型的字段和方法。那么father的类型表指针应该指向Father类型表,这样可以解释father调用父类的方法。那么字段呢?
      father is Son
      这又是如何实现的呢?
 
   3.值类型在编译期已经在栈上分配好了内存
    编译期还是IL指令,还没有转化为cpu可执行的二进制代码。这时内存是如何分配好的?
    编译期为什么要分配内存呢?不是应该运行的时候才需要么?
 
   4.您的书中在说明IL代码的时候多次提及计算堆栈。
     您能把IL代码执行的基本原理告诉我吗?或者从哪里可以看到相关的资料。
 
   5.对象实例存储在堆中,那么对象变量呢?是存储在线程堆栈中么?
 
    同时我也发现了您的一个疏漏。
 
第183页 第10行 首先调用父类构造函数,再调用子类构造函数。
第211页 第6行 在构造函数中,先初始化自身的字段,在调用基类的构造函数。
这两句表达有误。
应该是:
先调用子类构造函数,通过子类构造函数调用父类构造函数。先执行父类构造函数的代码,初始化父类的字段,再回到子类初始化子类的字段。
 
我编写了这样两个简单的类
 public class Father
    {
        protected string a;
 
        public Father()
        {
            a = "a";
        }
    }
 
    public class Son : Father
    {
        private string b;
 
        public Son()
        {
            b = a;
        }
    }
 
子类的构造函数IL代码如下:
 
.method public hidebysig specialname rtspecialname
        instance void  .ctor() cil managed
{
  // 代码大小       17 (0x11)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  call       instance void ConsoleApplication1.Father::.ctor()
  IL_0006:  nop
  IL_0007:  nop
  IL_0008:  ldarg.0
  IL_0009:  ldc.i4.1
  IL_000a:  stfld      int32 ConsoleApplication1.Son::b
  IL_000f:  nop
  IL_0010:  ret
} // end of method Son::.ctor
 
这段IL代码好很的证明了我的观点。
 
希望您能耐心的解答我的问题。谢谢
++++++++++++++++++++++++++++++++++++++++++
 
我的回复如下:
-----------------------------
1 “基类的实例字段”包括父类的private字段,但子类方法不能存取这个字段,这是由编译器在生成IL指令时保证的。
2    Son son=new Son();
      Father father=son;
     实际上,在内存中只有一个Son对象实例(放在堆中),但存在两个类型表(由CLR直接管理,当卸掉程序集时,这些类型表占用的资源被回收),赋值后,father和son存的是同一个首地址,指向在托管堆中的Son对象实例。
    father is son不是在程序运行时实现的,而是在编译时实现的,由C#编译器直接将这个代码翻译为IL指令,此IL指令会根据你的代码生成对合适方法的调用指令。
    当程序运行时,CLR直接装入的是翻译好的IL指令,而非C#代码,IL指令本身是没有“多态”特性的,因为它已经比较靠近底层,应该尽可能地简化以提高效率。
 
3 值类型在编译期已经在栈上分配好了内存
    这句是错的,变量的内存分配是在程序运行时才进行的。是我的疏漏。
 
4 CLR可以看成是一个基于堆栈的虚拟计算机,这台机器运行的是IL汇编程序,凡是IL代码中所说的堆栈,都是指“计算堆栈(Evaluation Stack)”。在书的附录中有一个“MSIL基础教程”,其中介绍了相关的原理。有关IL的资料很少,国内可以看到的书就是《Inside Microsoft  .NET IL Assembly》,千万别看中文版,我看译者肯定没弄明白其中的技术内容,译得一踏糊涂。要看就看英文版,但这本书阅读难度很大,作者是技术牛人,但作为一名作家,我认为不合格。本书中有关IL编程的介绍是我经过收集相关资料进行消化,并经实践检验之后写的,但管中窥豹,仅供参考。
 
5.你说得对:对象实例的数据存储在托管堆中,引用此对象的对象变量则是存储在线程堆栈中。
 
关于子类字段与父类字段初始化顺序的问题,我注意到你在子类构造函数中使用了基类的数据成员,因此才会导致先初始化基类数据成员,后初始化子类数据成员。这是一种特例。
事实上,如果子类字段与父类字段没有这种依存关系,C#编译器是按照以下顺序生成IL指令的:
new 子类对象时,子类构造函数被调用,在执行子类构造函数的代码时,先初始化子类的字段,然后再调用父类的构造函数初始化父类的字段。
如果子类字段依赖于父类字段的值,C#编译器在生成IL指令时,会先调用父类的构造函数初始化父类的字段,再调用子类构造函数初始化依赖于父类字段的这些字段。
我修改了一下你的代码,给子类和父类增加两个独立的字段:
 
   public class Father
    {
        protected string a;
 
        public int fatherFld=100;
 
        public Father()
        {
            a = "a";
        }
    }
 
    public class Son : Father
    {
        private string b;
 
        private int sonFld = 200;
        public Son()
        {
            b = a;
        }
    }
 
子类生成的IL代码如下:
.method public hidebysig specialname rtspecialname
        instance void  .ctor() cil managed
{
  // 代码大小       33 (0x21)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldc.i4     0xc8
  IL_0006:  stfld      int32 ConsoleApplication1.Son::sonFld
  IL_000b:  ldarg.0
  IL_000c:  call       instance void ConsoleApplication1.Father::.ctor()
  IL_0011:  nop
  IL_0012:  nop
  IL_0013:  ldarg.0
  IL_0014:  ldarg.0
  IL_0015:  ldfld      string ConsoleApplication1.Father::a
  IL_001a:  stfld      string ConsoleApplication1.Son::b
  IL_001f:  nop
  IL_0020:  ret
} // end of method Son::.ctor
可以看到IL_0006句先初始化子类的字段,IL_000c再调用父类的构造函数初始化父类字段,再回过头来于IL_0015和IL_001a两句完成用父类字段初始化子类字段的工作。
父类构造函数IL代码如下:
 
.method public hidebysig specialname rtspecialname
        instance void  .ctor() cil managed
{
  // 代码大小       29 (0x1d)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldc.i4.s   100
  IL_0003:  stfld      int32 ConsoleApplication1.Father::fatherFld
  IL_0008:  ldarg.0
  IL_0009:  call       instance void [mscorlib]System.Object::.ctor()
  IL_000e:  nop
  IL_000f:  nop
  IL_0010:  ldarg.0
  IL_0011:  ldstr      "a"
  IL_0016:  stfld      string ConsoleApplication1.Father::a
  IL_001b:  nop
  IL_001c:  ret
} // end of method Father::.ctor
注意一下基类object构造函数的调用是插在两个字段初始化指令中间的。我经过实验发现,C#编译器生成IL代码时对于字串类型字段的初始化总是在调用基类构造函数之后,而象int之类的字段,如果是独立的,其初始化指令总在调用基类构造函数指令之前。
为什么这样,只好去问问C#编译器的设计者了。
----------------------------
欢迎就此问题进行讨论。
posted on 2007-12-28 13:43  雷明  阅读(240)  评论(0编辑  收藏  举报