原则二:使用常量时,尽量使用readonly而不是const
C#有两种不同的常量:编译时常量( const)和运行时常量(readonly)。它们拥有非常不同的行为,不恰当的使用会造成你程序性能上和正确性上的损失。这两个方面的损失都不好,但是如果非要选一方的话,一个慢一点但是正确的程序要好过一个快一些但错误的程序。因为这个原因,比起编译时常量,你应该更喜欢运行时常量。编译时常量与运行时常量比起来会略微快一些,但是在灵活性上要差得多。相反的,运行时常量在性能上会差一些,但是能保证常量的值在不同的发布版本中不会改变。
我们用readonly关键字定义运行时常量,用const关键字定义编译时常量:
//编译时常量
public const int Millennium=2000;
//运行时常量
public static readonly int ThisYear=2011;//注原文是2004
上面这段代码展示了两种常量在类或结构体范围中是如何定义的。编译时常量可以定义在方法中,但是运行时常量就不能。
它们之间行为上的差别主要是由它们不同的访问方式而产生的。编译时常量在编译的时候会被它代表的值代替,如下:
if(myDateTime.Year==Millennium)
会与如下代码编译成同样的中间代码:
if(myDateTime==2000)
而运行时常量的值则是在运行的时候才能确定。一个运行时常量产生的中间代码是指向一个readonly变量,而不是一个具体的值。当你使用这两种常量的时候,这个区别就产生了一些不同的限制。编译时常量只能应用在原始数据类型(内建整性或浮点型)、枚举类型(enum)、或string类型。只有这些类型才能够使你在初始化的时候给常量赋予有意义的值。也只有这些原始类型才能够在编译成中间代码的时候用字面上的值来替代常量。下面这段代码是不能通过编译的。你不能用new操作符来初始化一个编译时常量,即便它的类型是值类型:
//无法通过编译,可以用readonly替代
public const DateTime classCreation = new DateTime(2000,1,1,0,0,0);
编译时常量的使用被限定在数字和string中。Readonly值一样也是常量,它们的值在构造函数执行后就不能再被修改了。但readonly的不同之处在于它的值是在运行时才被分配的。所以你在使用运行时常量的时候会有更大的灵活性。例如,运行时常量可以是任何类型的。你要在构造函数中对它进行初始化,或者你也可以使用初始化函数来做相初始化。你可以用readonly来定义一个DateTime结构体常量,但是用const却不行。
如果在类的实例中使用readonly值,你可以为一个类的每一个实例设置不同的数据值。而如果用编译时常量,只能通过定义静态常量。
最重要的区别是Readonly的值是在运行时才被实际赋值的。在中间代码中,当你指向一个readonly常量的时候,它指向的是一个readonly变量,而不是它的具体的值。随着时间的推移,这个特点在日后的维护中会显得越来越重要。编译时常量与你直接使用数字常量所产生的中间代码是一样的。即使是在跨程序集也是这样的:一个程序集中定义的常量在另一个使用到它的程序集中仍然是被同样的值所替代。
编译时常量与运行时常量值绑定方式的不同,会影响到它们运行时候的兼容性。假设你在名为Infrastructure的程序集中同时定义了const和readonly常量:
public class UsefulValues
{
public static readonly StartValue = 5;
public const EndValue =10;
}
在另一个程序集中,你使用到了这些值:
public void Print()
{
for(int i= UsefulValues.StartValue;int<UsefulValues.EndValue;int++)
{
Console.WriteLine("Value is {0}",i);
}
}
如果你跑一下程序,你将看到如下输出:
Value is 5
Value is 6
……
Value is 9
随着时间的流逝,你发布了一个新版本的Infrastructure程序集,它做了如下修改:
public class UsefulValues
{
public static readonly StartValue = 105;
public const EndValue =110;
}
你发布了新的Infrastructure程序集,而没有重新生成用到这些数据的应用程序集。你希望得到如下结果:
Value is 105
Value is 106
……
Value is 109
实际上,你根本不会得到任何输出结果。这个循环使用105作为它的开始值,而用10作为它的终结条件。C#编译器使用常量值10放入到应用程序的程序集中,而不是使用一个指向EndValue内存的指针。再看看StartValue,它被定义为readonly:在运行时才真正获得数值。所以,用到这些数据的应用程序集在不重新编译的情况下也可以获取到新的数值,简单地安装新版本的Infrastructure程序集,已经足够去改变所有用到这些数据的客户端的行为。更新公有的const常量的值应该被视为是接口的改变,你必须重新编译所有用到这个变量的代码。而更新readonly常量的值则是一个实施的改变,它的二进制代码与现有的客户端代码是兼容的。
从另一方面来说,有时候你确实是想要一些在编译时就确定值的东西。例如,一组用来标记一个对象的序列化版本的常量。标记具体版本(历史版本)的永久的值必须是一个编译时常量,他们不会发生改变。而目前版本则必须是一个运行时常量,它会随着每次新版本的发行而改变。
private const int Version1_0=0x0100;
private const int Version1_1=0x0101;
private const int Version1_2=0x0102;
//主版本发行
private const int Version2_0=0x0200;
private static readonly int CurrentVersion=Version2_0;
在每一个运行文件中,你都用运行时版本来存储目前版本号:
//读取存储内容,将存储版本与编译常量进行比较
protected MyType(SerializationInfo info,StreamingContext cntxt)
{
int storedVersion = info.GetInt32("VERSION");
switch(storedVersion)
{
case Version2_0:
readVersion2(info,cntxt);
break;
case Version1_1:
readVersion1Dot1(info,cntxt);
break;
}
}
//写入目前版本
[SecurityPermissionAttribute(SerityAction.Demand,SerializationFormater = true)]
void ISerializable.GetObjectData(SerializationInfo info,StreamingContext cntxt)
{
//用运行时常量作为目前版本
info.AddValue("VERSION",CurrentVersion);
//其它操作...........
}
Const对比readonly的最后一个优点是性能:一个是访问常量值,一个是对readonly变量的访问,前者能产生稍微高效一点的代码。尽管如此,这种性能上的改善只是很小的,但是灵活性却会变得更差,这是需要我们衡量的。如果你要获取这种性能的改善,就必须放弃灵活性。
当你用到必须和可选参数的时候,你将会遇到很多类似的在运行时还是编译时对常量值进行处理的抉择。在调用端,可选参数的默认值的设置与编译时常量的默认值设置一样。与处理readonly和const类似,你在改变可选参数的时候要非常小心(参见原则十,译注:目前未翻译,呵呵!先打个标!)。
当值必须在编译时确定的时候你只能使用const:如属性参数和枚举类型(enum)定义。那些在不同发布版本间定义的值不会发生改变的,也可以用const定义。除此之外,最好还是使用增加灵活性的readonly常量吧!
总结:第2节的翻译总算是快了一点。还是比较吃力,不过感觉会比第1节要好一些了。接下来的一个多星期会比较忙,估计第3节的翻译要过段时间才能出来了。见谅见谅!