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

JerryXia 发表于 , 阅读 (811)

对于常量,C#里有两个不同的版本:运行时常量和编译时常量。

因为他们有不同的表现行为,所以当你使用不当时,将会损伤程序性能或者出现错误。

两害相权取其轻,当我们不得不选择一个的时候,我们宁可选择一个运行慢一点但正确的那一个,而不是运行快一点但有错误的那个。基于这个理由,你应该选择运行时常量而不是编译时常量(译注:这里隐藏的说明了编译时常量效率更高,但可能会有错误)。

编译时常量更快更直接,但在可维护性上远不及运行时常量。保留编译时常量是为了满足那些对性能要求克刻,且随着程序运行时间的过去,其值永远不发生改变的常量使用的(译注:这说明编译时常量是可以不被C#采用的,但考虑到性能问题,还是做了保留)。

你可以用关键字readonly来声明(declare)一个运行时常量,编译时常量是用关键字const声明的。

//Compile time constant:
public cocnst int _Millennium = 2000;
//Runtime constant:
public static readonly int \_ThisYear = 2007;//(译注:原文为2004)

编译时常量与运行时常量不同之处表现在如何对他们的访问上。

一个编译时常量会被目标代码中的值直接取代。下面的代码:

if(myDateTime.Year == _Millennium)

会与下面写的代码编译成完全相同的IL代码:

if(myDateTime.Year == 2000)

运行时常量的值是在运行时确定的。当你引用一个只读常量时(read-only)IL会为你引用一个运行时常量的变量,而不是直接使用该值。

当你任意的使用其中一个常量时,这些区别就在一些限制上表现出来。编译时常量只能是基本类型(primitive
types)(built-in integral and floating-poing
types),枚举或者是字符串。这些就是你只能给运行时常量在初始化时赋值的类型。这些基本类就是可以被编译器在编译IL代码时直接用真实的值所取代的数据类型。下面的代码块(construct)不能通过编译。你不能用new运算符初始化一个编译时常量,即使这个数据类型是值类型。

//Does not complie, use readonly instead:
private const DateTime \_classCreation = new
DateTime(2000,1,1,0,0,0);

(译注:DateTime是一个值类型数据,但上面的代码因为用了new运算符,编译器无法在编译确定具体的对象应该用什么样的实际值来取代,所以无法通过编译。)

编译时常量仅限于数字和字符串。只读变量,也就是运行时常量,在构造函数(constructor)执行完成后它们是不以能被修改的。但只读变量是所有不同的,因为他们是在运行时才赋值的。当你使用运行时常量时,你有更大的可伸缩性。有一点要注意的是,运行时常量可以是任何类型的数据。而且你必须在构造函数里对他们初始化,或者你可以用任何一个初始化函数来完成。你可以添加一个DateTime结构的只读变量(--运行时常量),但你不能添加一个DateTime结构的(编译时)常量。

你可以把每一个实例(的常量)指定为只读的,从而为每一个类的实例存放不同的值。与编译时常量不同的是,它只能是静态的。

(译注:简单的讲,运行时常量可以是一个类的实例成员,也可以是一个类型的静态成员,而编译时常量只能是静态成员,因此类似:static
const string m_name;的代码是不能通过编译的。)

只读数据最重要的区别是他们在运行时才确定值。当你使用只读变量
时,IL会为你产生一个对只读变量引用,而不是直接产生数值。随着时间的推移,这个区别在(系统)维护上有深远的潜在影响。

编译时常量生成的IL代码就跟直接使用数值时生成的IL是一样的,即使是在跨程序集时:一个程序集里的编译时常量在另一个程序集会保留着同样的值(译注:这里说的不是很清楚,看后面的这个例子可能会更清楚一些)。

编译时常量和运行时常量的赋值方法对运行时的兼容性有所影响。

假设你已经在程序集Infrastructure中同时定义了一个const和一个readonly变量:

public class UserfulValues{
  public static readonly int StartValue = 5;
  public const int EndValue = 10;
}

同时,在另一个程序集(译注:这个程序集认为是我们做测试的应用程序的程序集,下面所说的应用程序的程序集都是指的这个程序集)中,你引用了这些值:

for(int i=UserfulValues.StartValue;i<UserfulValues.EndValue;i++){
    Console.WriteLine("value is {0}",i);
}

如果你运行这个简单测试程序,你可以看到下面明显的结果:

value is 5
value is 6
...
value is 9

过后,你又为程序集Infrastructure发布了个新的版本,并做了如下的修改:

public class UserfulValues{
    public static readonly int StartValue = 105;
    public const int EndValue = 120;
}

你单独的发布了程序集Infrastructure而没有全部编译你的程序,你希望得到下面的:

value is 105
value is 106
...
value is 119

事实上,你什么也得不到。上面的循环已经是用105开始而用10来结束。C#编译器(在编译时)把常量用10来代替应用程序的程序集中的使用,而不是用常量EndValue所存储的值。而常量StartValue的值,它是被申明为只读的,它可以在运行时重新读取该常量的值。因此,应用程序的程序集可以在不用重新编译的情况下使用新的数据,简单的编译一下Infrastructure程序集,然后重新布署安装一下,就足够让你的客户可能使用这些新的数据了。更新的编译时常量应该看成是接口的变化。你必须重新编译所有引用到编译时常量的代码。更新的运行时常量则可以当成是实现的改变,这于在客户端已经存在的二进制代码是兼容的。用MSIL解释一下前面的那个循环里发生了什么:

IL_0000: ldsfld int32 Chapter1.UserfulValues::StartValue
IL_0005: stloc.0
IL_0006: br.s IL_001c
IL_0008: ldstr "value is {0}"
IL_000d: ldloc.0
IL_000e: box [mscrolib]System.Int32
IL_0013: call void
[mscrolib]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(的值)是动态载入的。

但是,在MSIL命令的最后,结束条件是把值10当成硬代码(hard-coded)使用的。

另一方面,有些时候你也须要为某些值使用编译时常量。例如:考虑一个须要识别不同版本的续列化情形。用来标识一个特殊版本号的常量应该是一个编译时常量,它们决不会发生改变。而当前版本号则应该是一个运行时常量,在不同的版本发布后会有所改变。

private const int VERSION_1_0 = 0x0100;
private const int VERSION_1_1 = 0x0101;
private const int VERSION_1_2 = 0x0102;
//major release;
private const int VERSION_2_0 = 0x0200;
//Chech for the current version:
private static readonly int CURRENT_VERSION = VERSION_2_0;

在每次存盘时,你用运行常量来保存当前版本号。

//Read fom persistent storage, check stored version against
complie-time constant:
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:
    readVersion1(info,cntxt);
    break;
    //etc.  
  }
}

//Write the current version:

[SecurityPermissionAttribute(SecurityAction.Demand,SerializationFormatter = true)]
void ISerializable.GetObjectData(SerializationInfo
inf,StreamingContext cxt){
  //use runtime constant for currnet version
  inf.AddValue("VERSION",CURRENT_VERSION);
  //
  //write remaining delements...
}

最后一个有利的原因而使我们要使用编译时常量,就是它的性能。比起运行时常量,已知的编译时常量可以更直接有效的被访问。然而,性能上的功效是甚微的,并且应该与可伸缩性的降低进行一个权衡。Be
sure to profile performace differences before giveing up the
flexibility.

const的值必须在编译时被确定,(它们可以是):属性参数,枚举定义,以及一小部份你认为应该定义一个值且该值不能在不同的版本发布时发生改变的常量。

无论如何,宁愿选择伸缩性更强的运行时常量。

============================

小结:第一次翻译。昨天花了半天时间,今天又花了几个小时。终于完成了Item2的翻译。感觉很吃力。或者我的翻译很糟糕,或者已经有更好的翻译了,但不管怎样,这也算是我自己辛苦劳动的结果。希望对读者有所帮助。本想在翻译中添加一我自己的看法,那样的话对原文的改动可能比较大,所以没做太多注解。我会继续努力翻译完后面的Items,不管翻译的怎样,也不管是不是有其它的翻译已经完成了。这对自己也是一个学习过程。我会在后面的翻译中一些我自己的看法,当然尽可能的保留原文的样子,实在不好办的,我就会以读书笔记的形式另外写文章。

另外,我也不知道这样的文章能不能发在首页,如果不行,以后的就都发到其它区吧。

添加新评论