改善C#程序的50种方法 条款2:运行时常量(readonly)优于编译时常量(const)

转载 2007年09月21日 10:40:00

C#语言有两种不同的常量机制:一种为编译时 (compile-time)常量,一种为运行时(runtime)常量。两种常量有着非常迥异的行为,使用不正确会导致程序的性能下降或者出现错误。这 两种代价,哪一个都没有人愿意承担,但是如果必须承担一个,那么“慢、但是能够正确运行的”程序总比“快、但是可能出错的”程序要好。因此,我们说运行时 常量优于编译时常量。编译时常量比运行时常量稍微快一点,但却缺乏灵活性。只有在性能非常关键,并且其值永远不会改变的情况下,我们才应该使用编译时常 量。

在C#中,我们使用readonly关键字来声明运行时常量,用const关键字来声明编译时常量。

// 编译时常量:

public const int _Millennium = 2000;

// 运行时常量:

public static readonly int _ThisYear = 2004;

编译时常量与运行时常量行为的不同处在于它们的访问方式。编译时常量在编译后的结果代码中会被替换为该常量的值,例如下面的代码:

if ( myDateTime.Year == _Millennium )

其编译后的IL和下面的代码编译后的IL一样:

if ( myDateTime.Year == 2000 )

条款2:运行时常量(readonly)优于编译时常量(const)  16

 
运行时常量的值则在运行时被计算。对于使用运行时常量的代码,其编译后的IL将维持对readonly变量(而非它的值)的引用。

这种差别会为我们使用两种常量类型带来一些限制。编译时常量只可以用于基元类型(包括内建的整数类型和浮点类型)、枚举类型或字符串类型。因为只有这些类型才允许我们在初始化器中指定有意义的常量值[2]。在使用这些常量的代码编译后得到的IL代码中,常量将直接被替换为它们的字面值(literal)。例如,下面的代码就不会通过编译。事实上,C#不允许我们使用new操作符来初始化一个编译时常量,即使被初始化的常量类型为一个值类型。

// 下面的代码不会通过编译,但是换成readonly就可以:

private const DateTime _classCreation = new

  DateTime( 2000, 1, 1, 0, 0, 0 );

编译时常量仅限于数值和字符串。只读(read- only)字段之所以也被称作一种常量,是因为它们的构造器一旦被执行,我们将不能对它们的值做任何修改。与编译时常量不同的地方在于,只读字段的赋值操 作发生在运行时,因此它们具有更多的灵活性。比如,只读字段的类型就没有任何限制。对于只读字段,我们只能在构造器或者初始化器中为它们赋值。在上面的代 码中,我们可以声明readonly的DateTime结构变量,但是却不能声明const的DateTime结构变量。

我们可以声明readonly的实例常量,从而为一个类型的每个实例存储不同的值。但是const修饰的编译时常量默认就被定义为静态常量。

我们知道,运行时常量和编译时常量最重要的区别就 在于运行时常量值的辨析发生在运行时,而编译时常量值的辨析发生编译时。换言之,使用运行时常量编译后的IL代码引用的是readonly变量,而非它的 值;而使用编译时常量编译后的IL代码将直接引用它的值——就像我们直接在代码中使用常量值一样。即使我们使用的是数值常量并跨程序集引用,情况也是一 样:如果在程序集A中引用程序集B中的常量,那么编译后程序集A中出现的那个常量将被它的值所替换。这种差别对于代码的维护性而言有着相当的影响。

 

编译时常量与运行时常量被辨析的方式影响着运行时的兼容性。假设我们在一个名为Infrastructure 的程序集中分别定义了一个const字段和一个readonly字段:

public class UsefulValues

{

  public static readonly int StartValue = 5;

  public const int EndValue = 10;

}

在另外一个程序集Application中,我们又引用着这些值:

for ( int i = UsefulValues.StartValue;

  i < UsefulValues.EndValue;

  i++ )

  Console.WriteLine( "value is {0}", i );

如果我们运行上面的代码,将得到以下输出:

Value is 5

Value is 6

...

Value is 9

假设随着时间的推移,我们又发布了一个新版的Infrastructure程序集:

public class UsefulValues

{

  public static readonly int StartValue = 105;

  public const int EndValue = 120;

}

我们将新版的Infrastructure程序集分发出去,但并没有重新编译Application程序集。我们期望得到如下的输出:

Value is 105

Value is 106

...

Value is 119

但实际上,我们却没有得到任何输出。因为现在那个循环语句将使用105作为它的起始值,使用10作为它的结束 条件。其根本原因在于C#编译器在第一次编译Application程序集时,将其中的EndValue替换成了它对应的常量值10。而对于 StartValue来说,由于它被声明为readonly,所以它的辨析发生在运行时。因此,Application程序集在没有被重新编译的情况下, 仍然可以使用新的StartValue值。为了改变所有使用readonly常量的客户代码的行为,简单地安装一个新版的Infrastructure 程序集就足够了。“更改一个运行时常量的值”应该被视作对类型接口的更改,其后果是我们必须重新编译所有引用该常量的代码。“更改一个公有的运行时常量的 值”应该被视作对类型实现的更改,它与其客户代码在二进制层次上是兼容的。大家看看上述代码中的循环编译后的MSIL,就会对这里所谈的更加清楚了:

IL_0000:  ldsfld     int32 Chapter1.UsefulValues::StartValue

IL_0005:  stloc.0

IL_0006:  br.s       IL_001c

IL_0008:  ldstr      "value is {0}"

IL_000d:  ldloc.0

IL_000e:  box        [mscorlib]System.Int32

IL_0013:  call       void [mscorlib]System.Console::WriteLine

    (string,object)

IL_0018:  ldloc.0

IL_0019:  ldc.i4.1

IL_001a:  add

IL_001b:  stloc.0

IL_001c:  ldloc.0

IL_001d:  ldc.i4.s   10

IL_001f:  blt.s      IL_0008

大家可以在这段MSIL代码的顶端看到StartValue的确是被动态加载的,而在其末尾可以看到结束条件被硬编码(hard-code)为10。

不过,有时候有些值确实可以在编译时确定,这时候 就应该使用编译时常量。例如,考虑在对象的序列化形式(有关对象序列化,可参见条款25)中使用一组常量来区分不同版本的对象。其中,标记特殊版本号的持 久化数据应该采用编译时常量,因为它们的值永远不会改变。但是标记当前版本号的数据应该采用运行时常量,因为它的值会随着每个不同的版本而改动。

private const int VERSION_1_0 = 0x0100;

private const int VERSION_1_1 = 0x0101;

private const int VERSION_1_2 = 0x0102;

// 主发行版本:

private const int VERSION_2_0 = 0x0200;

// 标记当前版本:

private static readonly int CURRENT_VERSION =

  VERSION_2_0;

我们使用运行时版本[3]来将当前的版本号存储在每一个序列化文件中:

// 从持久层数据源读取对象,将存储的版本号与编译时常量相比对:

protected MyType( SerializationInfo info,

  StreamingContext cntxt )

{

  int storedVersion = info.GetInt32( "VERSION" );

  switch ( storedVersion )

  {

  case VERSION_2_0:

    readVersion2( info, cntxt );

    break;

  case VERSION_1_1:

    readVersion1Dot1( info, cntxt );

    break;

  // 忽略其他细节。

  }

}

// 写入当前版本号:

[ SecurityPermissionAttribute( SecurityAction.Demand,

  SerializationFormatter =true ) ]

void ISerializable.GetObjectData( SerializationInfo inf,

    StreamingContext cxt )

{

  // 使用运行时常量来标记当前版本号:

  inf.AddValue( "VERSION", CURRENT_VERSION );

  // 写入其他元素……

}

 
使用const较之于使用readonly的唯一好处就是性能:使用已知常量值的代码效率要比访问readonly值的代码效率稍好一点。但是这其中的效 率提升是非常小的,大家应该和其所失去的灵活性进行一番权衡比较。在打算放弃灵活性之前,一定要对两者的性能差别做一个评测。

综上所述,只有当某些情况要求变量的值必须在编译时可用,才应该考虑使用const,例如:特性(attribute)类的参数,枚举定义,以及某些不随组件版本变化而改变的值。否则,对于其他任何情况,都应该优先选择readonly常量,从而获得其所具有的灵活性。

Java虚拟机-----方法区和运行时常量池

方法区: 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据(是唯一的数据)。当java虚拟机通...

方法区和运行时常量池溢出

方法区和运行时常量池溢出 由于运行时常量池是方法区的一部分,因此这两个区域的溢出测试就放在一起进行。前面提到JDK 1.7开始逐步“去永久代”的事情,在此就以测试代码观察一下这件事对程序的实际影...

Java方法区和运行时常量池溢出问题分析

运行时常量池是方法区的一部分,方法区用于存放Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。   String.intern()是一个native方法,它的作用是:如果字...

Java虚拟机-----方法区和运行时常量池

方法区: 方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的 代码等数据(是唯一的数据)。当java...

虚拟机运行时常量池与String的intern方法

运行时常量池是方法区的一部分,存放编译期生成的字面量和符号引用,String的intern()方法在运行期间将新常量放入运行时常量池中 在程序的编译期,生成了class文件,代码中的类 ,方...

Java虚拟机-----方法区和运行时常量池

转载自:http://blog.csdn.net/sunshine__me/article/details/49992909 方法区: 方法区(Method Area)与Java堆一样,是各...

方法区和运行时常量池溢出

由于运行时常量池是方法区的一部分,因此这两个区域的溢出测试就放在一起进行。 如果要向运行时常量池中添加内容,最简单的做法就是使用String.intern()这个Native方法。该方法的作用是:如...
  • pfnie
  • pfnie
  • 2016年10月09日 18:40
  • 232

JVM 内存基础概念之 方法区和运行时常量池

方法区和运行时常量池 方法区 方法区和 Java 堆一样,是被所有线程所共享的一块内存区域。 方法区的作用是存储已经被 JVM 加载到方法区之中的 Java 类的类型信息。 前面我们多次提到了类的实...
  • Airsaid
  • Airsaid
  • 2016年02月23日 16:28
  • 1962

JVM运行时数据区介绍[堆、栈、方法区、常量池]

程序计数器:当前线程所执行的字节码的行号指示器。 栈: 栈内存、虚拟机栈、或者说是虚拟机栈中的局部变量表。 用于存放编译期可知的各种 基本数据类型、对象引用类型[ 她可能是一个引用指针、也...

原则2:为你的常量选择readonly而不是const

原则2:为你的常量选择readonly而不是const Preferreadonly to const 对于常量,C#里有两个不同的版本:运行时常量和编译时常量。 因为他们有不同的表现行为,所以...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:改善C#程序的50种方法 条款2:运行时常量(readonly)优于编译时常量(const)
举报原因:
原因补充:

(最多只允许输入30个字)