探索系列:深入辨析 ReadOnly,Const


      Last time when I was asked what is the diff between Readonly and ConstI was really ashamed that I said don’t know on explaining the exact difference between this two simple key word…

过后,特意研究了下这两个关键字究竟有何不同:

 

首先,从CLI中对CTSType的规定说起。在第四版本的ECMA-355CLI标准中,规定的对于每一个Type,都可以包含很多成员。所有的成员(members),一共可以有三种:第一种叫做fields,它是定义的和这个类型相关的存储单元。第二中成员类型就是我们熟悉的方法(Method)。第三中,叫做nested type。也就是嵌套类型,对于每一个type里面,都可以包含对于别的type的声明和使用。

对于所有的成员,又可以分为两种:per-type memberper-instance member。顾名思义,前一种,是对于每一个type只有一个的成员,譬如static修饰的field。后一种,是对于每一个运行时候的实例,都保持一个不同的实现版本。

对于其他的类型的成员,包括我们熟知的PropertiesEvents之类,这些反编译成为IL语言之后可以看到,全部直接的,或者是间接的转换成为了方法。

 

有的时候,我们需要一个field在它的整个生命周期里面,它所包含的这个值是不改变的。这个时候,根据这个变量被赋值的不同的情况,CLR为提供了两种技术来实现这种要求:

第一种,field里面包含的值,可以在编译的时候被计算出来。就是const关键字声明的变量。这种方法的实现效率是最高的。这个值,是作为一个字面上的固定的值,保存在包含这个typemodule文件的metadata里面。Const定义的这个值,可以是一个表达式,但是这个表达式的结果,必须是编译的时候可以计算出来的。这样,在编译的时候,就可以植入到其余的指令里面去。

Const定义的field,必须在定义的时候,就给初始化了,而且,一旦初始化了之后,就不能再改变它的值。同时,不能声明一个constfieldstatic,因为,const定义的field就表明这个field是一个static的。

任何对const定义的常量的修改,都会抛出一个编译时错误。

我们可以把const修饰的field,归类到memberper-type member里面去。

第二种,是被readonly修饰的fieldReadonly修饰的常量,可能是因为,有的时候,我们需要一些field,它的值是不应该改变的,但是,在运行之前,是不知道这个值的,这个时候,就可以用readonly来修饰这个field

这个readonly修饰的field,就为常量这个名词提供了另外一种灵活得多的解决方案了。首先,它的值,是可以在运行的时候,根据别的变量动态计算出来的,而const修饰的常量的值就不能这样了。同时,不同于const,它是一个per-instance member。也就是说,对于每一个实例,它都可以保存一个不同的实现版本,而const就不行了。对于某一个type的所有的实现实例,const定义的field,只能有一个不变的值。

 

写到这里,从原理的角度已经阐述的比较清楚了,接着用一个实例来分析下:

    class Program

    {

        public const int conField=122*1119;

        public readonly int roField;

        private int _property;

        public int Property

        {

            get

            {

                return _property;

            }

            set

            {

                _property = value;

            }

        }

 

        static void Main(string[] args)

        {

            (new Program()).Method();

        }

}

Diassemble之后,得到下面的显示结果:

.namespace TestConcoleApp

{

  .class private auto ansi beforefieldinit Program

       extends [mscorlib]System.Object

  {

       //这里可以看到,一个const定义的一个field,是literal,并且被static关键字来修饰。同时,在编译的时候,就计算得到了这个field的值。

    .field public static literal int32 conField = int32(136518)

       //readonly关键字修饰的field,只是被用initonly来修饰,per-instance类型。

    .field public initonly int32 roField

    .field private int32 _property

       //在程序的属性里面定义的get关键字,最终在这里被转换成为了一个方法来处理。

    .method public hidebysig specialname instance int32 get_Property() cil managed

    {

      .maxstack 1

      .locals init (int32)

 

      IL_0000:  nop       

      IL_0001:  ldarg.0   

      IL_0002:  ldfld      int32 TestConcoleApp.Program::_property

      IL_0007:  stloc.0   

      IL_0008:  br.s       IL_000a

      IL_000a:  ldloc.0   

      IL_000b:  ret       

    }

       //对上面的一个Propertyset动作,最终在这里转化成为了一个方法

    .method public hidebysig specialname instance void set_Property(int32 'value') cil managed

    {

      .maxstack 8

     

      IL_0000:  nop        

      IL_0001:  ldarg.0   

      IL_0002:  ldarg.1   

      IL_0003:  stfld      int32 TestConcoleApp.Program::_property

      IL_0008:  ret       

    }

 

    .method private hidebysig static void Main(string[] args) cil managed

    {

      .entrypoint

      .maxstack 8

     

      IL_0000:  nop       

      IL_0001:  newobj     instance void TestConcoleApp.Program::.ctor()

      IL_000b:  nop       

      IL_000c:  ret       

    }

 

    .method public hidebysig specialname rtspecialname instance void .ctor() cil managed

    {

      .maxstack 8

     

      IL_0000:  ldarg.0   

      IL_0001:  call       instance void [mscorlib]System.Object::.ctor()

      IL_0006:  ret       

    }

       //这里用property来表示一个属性,实际上是转换成为了两个方法来操作一个private修饰的field

    .property instance int32 Property()

    {

      .get instance int32 TestConcoleApp.Program::get_Property()

      .set instance void TestConcoleApp.Program::set_Property(int32)

    }

  }

}

 

再看了readonlyconst被编译成为IL的代码之后,对这两个关键字到底是如何运作,有什么区别和相同的地方,又有了进一步的确认和了解。

 

后记:

其实,我还想验证一下,public const int conField=122*1119;这一句中122*1119的结果,在作为一个module的时候,是计算好了保存在一个扩展了的DotNet下的PE文件的Metadata里面,然后在验证一下,运行的时候,一个type有多个实例,而const定义的field只有一个的。

不过动用了一大批调试工具和托管模块结构查看工具,把PE格式文件里面的元数据表狠狠的都刨了一遍,也没找到这个值。在这个文件执行的时候,windbg attach到这个进程,把这个模块在的内存刨了一个遍,还是没找到这个常量具体的location….郁闷坏了

先就整理到这里吧,改天好好的分析下PE文件格式里面的元数据表和托管进程的内存布局。

      

后记补记:

今天,又把metadata的几种表结构的reference看了看,把上面生成的那个托管模块里里外外又刨了一次,用dumpbin+ildasm,在查看元数据表的时候,终于找到了上面用readonlyconst定义的两个字段在元数据里面的表示:

       Field #1 (04000001)

       -------------------------------------------------------

              Field Name: conField (04000001)

              Flags     : [Public] [Static] [Literal] [HasDefault]  (00008056)

       DefltValue: (I4) 136518

              CallCnvntn: [FIELD]

              Field type:  I4

 

       Field #2 (04000002)

       -------------------------------------------------------

              Field Name: roField (04000002)

              Flags     : [Public] [InitOnly]  (00000026)

              CallCnvntn: [FIELD]

              Field type:  I4

       可以看到,在编译成为托管模块的时候,这个const定义的变量就被计算了出来,把值136518已经保存到了托管PE文件的元数据表里面。并且,还有staticliteralhasdefault这几个关键字来修饰。而readonly修饰的field,只有一个initonly来修饰。



      
到这里,终于给这段刨根问底readonlyconst关键字的文章画上了一个比较完整的句号吧。

posted on 2007-12-23 00:38  lbq1221119  阅读(1236)  评论(5编辑  收藏  举报

导航