Effective C#: Item 2 Prefer readonly to const
Item 2: 定义常量时,优先使用readonly,而不是const
在C#中,有两种类型的常量,compile-time和runtime。它们的性质是差别很大的。正确的选择对于你程序的稳定性和性能都有很大的影响。Compile-time常量速度稍稍快一点,但灵活性非常的差,而且容易出错。当鱼和熊掌不可兼得的情况下,我们应该选择一个稍慢一点,但更加稳定和正确的方式来编写程序。只有当性能是程序决定因素的时候才使用Compile-time类型常量,它的值永远不变。
readonly 用来定义runtime类型常量;const 用来定义compile-time常量:
//compile time constant:
public const int _Millennium = 2000;
//runtime constant:
public static readonly int _ThisYear = 2005;
compile-time常量和runtime常量性质的不同是由访问它们方式的不同决定的。Compile-time常量只是简单的值的替换。下面两段代码的IL code是相同的:
if( myDateTime.Year == _Millennium )
if( myDateTime.Year == 2000 )
runtime常量只有在runtime才被解析。此时你reference的是一个read-only的常量,这个read-only的常量reference一个readonly的变量,而不是变量的值。
这种分别决定了你何时使用这两种类型的常量。Compile-time常量只能使用在primary类型上(比如built-in int, float)。因为只有这些类型才能在初始化时赋有意义的值。Compile-time常量是不能用new来赋值的:
//Does not compile, use readonly instead
private const DateTime _classCreation =
new DateTime( 2000, 1, 1, 0, 0, 0 );
所以compile-time常量只能是数值和字符串。
Runtime常量的值在constructor运行后也是不可变的,但它们的赋值是在runtime进行的。所以也就有了更大的灵活性。runtime常量的值可以是任何类型,但你必须在constructor或initializer中给它们赋值。
你可以给实例常量赋readonly的值,这样类的每个实例可以有不同的值。当然你也可以把readonly常量定义为static. Compile-time常量则是static常量,定义是不用加static 的。
因为runtime常量只有在runtime才被解析。你所reference的是一个read-only的常量,这个read-only的常量reference一个readonly的变量,而不是变量的值。所以使用它可以给维护程序带来极大的便利。需要牢记的是,compile-time常量永远都是被值代替,即使是在一个assembly中定义,在另外一个assembly使用。这使得维护非常的不便。
比如你开发了一个assembly A,在这个assembly A中定义如下的两个常量:
public class UsefulValues
{
public static readonly int StartValue = 5;
public const int EndValue = 10;
}
在另外一个assembly B中你使用了这两个常量:
for( int i=UserfulValues.StartValue;
i<UsefulValues.EndValue; i++)
{
Console.WriteLine( "Value is {0}", i );
}
运行程序,你会得到如下输出:
Value is 5
Value is 6
…
Value is 9
过了一段时间后,你修改了assembly A:
public class UsefulValues
{
public static readonly int StartValue = 105;
public const int EndValue = 120;
}
你发布了assembly A,但并没有重新编译整个程序,你认为你程序的输出会是:
Value = 105
Value = 106
…
Value = 119
但实际情况并非如此。因为你没有重新编译,所以在assembly B中,StartValue变成了105,但EndValue仍然是10,所以你的程序不会输出任何东东。因为StartValue是在runtime解析的,所以不需重新编译整个程序,但EndValue是compile-time常量,所以没有变化。改变compile-time常量可以被看作是interface的改变,必须要重新编译程序中所有使用到它的code。但改变runtime常量的值可以被看作是implementation的改变,它具有binary的兼容性。也就是说改变interface的实现方法对于interface的用户是没有影响的,所以不用重新编译。你可以用reflector来观察IL code的不同。在IL中,StartValue是动态载入的,而EndValue是hard-coded。
但有些时候,你确实需要compile-time常量。比如在对一个object serialize时,你想用一些常量来描述object不同的版本信息,这些信息是永远不变的。但你需要一个runtime常量来描述当前版本信息。当前版本信息是随着新的release而改变的。
private const int VERSION_1_0 = ox0100;
private const int VERSION_1_1 = ox0101;
private const int VERSION_1_2 = ox0102;
//major release:
private const int VERSION_2_0 = ox0200;
//check for the current version
private static readonly int CURRENT_VERSION = VERSION_2_0;
//read from file, check stored version
//against compile-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:
ReadVersion1Dot1( info, cntxt );
break;
//...
}
}
//write the current version
[ SecurityPermissionAttribute( SecurityAction.Demand,
SerilizationFormatter = true ) ]
void ISerializable.GetObjectData( SerializationInfo inf,
StreamingContext cxt )
{
//user runtime constant for current version
inf.AddValue( "VERSION", CURRENT_VERSION );
//...
}
因为runtime常量要进行一次reference访问,所以compile-time常量的效率稍稍好一点。但这是以牺牲 重要的灵活性为代价的。所以决定用const而放弃readonly之前,最好用profiler监测一下两种情况下的性能差别,以确定这样做确实值得。
总之,只有当你必须要在compile时确定值的情况下,比如修饰function内部的局部变量(readonly只可以用于修饰类的成员),attribute参数,枚举定义等等,才使用const。记住,这些值是永远不变的。其他一切情况下,使用readonly来 最大的提高灵活性。
本系列文章只是作者读书笔记,版权完全属于原作者 (Bill Wagner),任何人及组织不得以任何理由以商业用途使用本文,任何对本文的引用和转载必须通知作者:zphillm@hotmail.com