# 原则2:偏爱 readonly 而不是 const
**By D.S.Qiu**
尊重他人的劳动,**支持原创,转载请注明[出处](/blog/1976703):[http://dsqiu.iteye.com](http://dsqiu.iteye.com)**
C# 有两种常量:编译时常量和运行时常量。它们有不同的行为,不当使用会花费性能或出现错误。如果不得不选择其中一个,宁可是一个慢一点但正确的程序,而不是一个快速但会出错的程序。出于这个考虑,你应该更偏向于运行时常量而不是编译时常量。编译时常量会比编译时常量稍微快点,但更不灵活。只有当性能是一个致命因素而且要求常量不随版本发生改变时才会保留编译时常量。
你可以使用 readonly 关键字声明运行时常量。使用 const 关键字声明编译器常量:
```
// Compile time constant:
public const int Millennium = 2000;
// Runtime constant:
public static readonly int ThisYear = 2004;
```
上面的代码可以出现类或结构体的作用域(scope)中。编译时常量还可以在方法体中声明。运行时常量不能在方法体重声明。
编译时常量和运行时常量访问方式不同导致不同的行为。在目标代码中编译时常量会被替换成常量值。比如下面代码:
```
// Compile time constant:
public const int Millennium = 2000;
// Runtime constant:
public static readonly int ThisYear = 2004;
```
会和下面写法的编译的 IL 代码是一样的:
```
if (myDateTime.Year == 2000)
```
运行时常量的值是在运行时得到的。当你引用一个只读(read-only)常量, IL 会引用一个 readonly 变量而不是直接使用值。
使用编译时常量和运行时常量还有不同的限制。编译时常量只能在基本类型(内建整数和浮点数类型),枚举类型,或字符串。编译时常量要求类能用有意义的常量赋值初始化。而只有基本类型才能在 IL 代码中使用常量(literal values)来替换。不能使用使用 new 操作法初始化编译时常量,即使它是一个值类型:
```
// Does not compile, use readonly instead:
private const DateTime classCreation = new
DateTime(2000, 1, 1, 0, 0, 0);
```
编译时常量只能使用与数字和字符串。只读(Read-only)变量也是常量,即不能在构造函数完成之后再修改。但只读变量是在运行时赋值。这会比编译时常量更灵活。首先,运行时常量可以是任何类型。你必须在构造函数或者直接初始化。你可以让 DateTime 结构体变为 readonly 值;但不能使用 const 创建 DateTime 值。
readonly 值可以是实例类型(instance)常量,让一个类的实例存储不同值。而编译时常量则是被定义为 static 常量的。
只读变量最重要的不同在于运行时才确定值。当你引用一个只读变量, IL 会为你产生一个指向只读变量的引用,而不是值。这种差异将对维护上产生深远的影响。编译时常量产生的 IL 代码就跟直接使用数值变量时一样的,即使是跨程序集:一个程序集的常量在另一个程序集还是被替换为数值。
编译时常量和运行时常量的赋值方式会影响运行时的兼容性。假设你在程序集 Infrastructure 中同时定义了 const 和 readonly 域:
```
public class UsefulValues
{
public static readonly int StartValue = 5;
public const int EndValue = 10;
}
```
在另外一个程序集,你引用这两个值:
```
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 而没有重新编译全部应用程序。你希望得到下面的结果:
```
Value is 105
Value is 106
...
Value is 119
```
实际上,你不会得到任何输出。循环条件开始于105,结束于10。 C# 编译器用10替换应用程序集的 const 变量而不是指向存储 EndValue 的引用。 StartValue 的情况趋势截然不同。因为它被声明为 readonly :在运行时确定值。因此,应用程序集能不用重新编译就能充分利用新值;只要很简单地按照更新版本的 Infrastructure 程序集就可以改变使用该变量的值。更新 public const 变量的值应该当做接口的变化。更新只读常量的值只是实现的改变,兼容客户端的二进制代码。
另一方法,有时候某些值的确需要是编译时常量。例如:考虑使用编译时常量标记对象的序列化版本(查看原则27)。标记特定版本号的持久化值要使用编译时常量,它们决不会发生改变。但当前的版本号应该是一个运行时常量,随着版本不同而改变。
```
private const int Version1_0 = 0x0100;
private const int Version1_1 = 0x0101;
private const int Version1_2 = 0x0102;
// major release:
private const int Version2_0 = 0x0200;
// check for the current version:
private static readonly int CurrentVersion =
Version2_0;
```
你会使用运行时常量存储每个文件的当前版本号:
```
// Read from persistent storage, check
// stored version against compile-time constant:
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;
// etc.
}
}
```
```
// Write the current version:
[SecurityPermissionAttribute(SecurityAction.Demand,
SerializationFormatter = true)]
void ISerializable.GetObjectData(SerializationInfo inf,
StreamingContext cxt)
{
// use runtime constant for current version:
inf.AddValue("VERSION", CurrentVersion);
// write remaining elements...
}
```
相比 readonly ,使用 const 最后的一个优势就是性能:已知的常量值会比使用变量访问的 readonly 变量产生稍微高效的代码。然而,性能上甚微的收效和灵活性的减小应该做一个很好的权衡。放弃灵活性之前一定要剖析性能差异。可选参数的默认值会在调用时会像编译时变量(声明为 const 的变量)一样被替换成默认值。和使用 readonly 和 const 变量一样,你要非常认真对待可选参数值的不同。(查看原则10。)
当你使用命名(named)参数和可选(optial)参数时,你会遇到和使用运行时常量和编译时常量一样的权衡。
当在编译时期必须要获得变量的值时必须使用 const :特性(attribute)参数和枚举定义,以及当你定义一个不随版本的变化而变化的值得罕见的时候。无论如何,更偏爱于只读常量的更强的灵活性。
小结:
第二节字数相对少些,所以今天虽然颈椎有点不舒服(千万不要有事呀,我还没有疯够),根据以前的节奏(时间还早),还可以干点别的,或者躲进被窝理顺事情,每天给自己思考的事件太少了,没有思考,积淀就会来的慢,这个跟前面强调的不同哈。
欢迎各种不爽,各种喷,写这个纯属个人爱好,秉持”分享“之德!
有关本书的其他章节翻译请[点击查看](/category/297763),转载请注明出处,尊重原创!
如果您对D.S.Qiu有任何建议或意见可以在文章后面评论,或者发邮件(gd.s.qiu@gmail.com)交流,您的鼓励和支持是我前进的动力,希望能有更多更好的分享。
转载请在**文首**注明出处:[http://dsqiu.iteye.com/blog/1976703](/blog/1976703)
更多精彩请关注D.S.Qiu的 博客 和微博(ID:静水逐风)
- 第一章 C# 语言习惯
- 原则1:使用 属性(Poperty)代替可直接访问的数据成员(Data Member)
- 原则2:偏爱 readonly 而不是 const
- 原则3:选择 is 或 as 而不是强制类型转换
- 原则4:使用条件特性(conditional attribute)代替 #if
- 原则5:总是提供 ToString()
- 原则6:理解几个不同相等概念的关系
- 原则7:明白 GetHashCode() 的陷阱
- 原则8:优先考虑查询语法(query syntax)而不是循环结构
- 原则9:在你的 API 中避免转换操作
- 原则10:使用默认参数减少函数的重载
- 原则11:理解小函数的魅力
- 第二章 .NET 资源管理
- 原则12:选择变量初始化语法(initializer)而不是赋值语句
- 原则13:使用恰当的方式对静态成员进行初始化
- 原则14:减少重复的初始化逻辑
- 原则15:使用 using 和 try/finally 清理资源
- 原则16:避免创建不需要的对象
- 原则17:实现标准的 Dispose 模式
- 原则17:实现标准的 Dispose 模式
- 原则18:值类型和引用类型的区别
- 原则19:确保0是值类型的一个有效状态
- 原则20:更倾向于使用不可变原子值类型
- 第三章 用 C# 表达设计
- 原则21:限制你的类型的可见性
- 原则22:选择定义并实现接口,而不是基类
- 原则23:理解接口方法和虚函数的区别
- 原则24:使用委托来表达回调
- 原则25:实现通知的事件模式
- 原则26:避免返回类的内部对象的引用
- 原则27:总是使你的类型可序列化
- 原则28:创建大粒度的网络服务 APIs
- 原则29:让接口支持协变和逆变
- 第四章 和框架一起工作
- 原则30:选择重载而不是事件处理器
- 原则31:用 IComparable&lt;T&gt; 和 IComparer&lt;T&gt; 实现排序关系
- 原则32:避免 ICloneable
- 原则33:只有基类更新处理才使用 new 修饰符
- 原则34:避免定义在基类的方法的重写
- 原则35:理解 PLINQ 并行算法的实现
- 原则36:理解 I/O 受限制(Bound)操作 PLINQ 的使用
- 原则37:构造并行算法的异常考量
- 第五章 杂项讨论
- 原则38:理解动态(Dynamic)的利与弊
- 原则39:使用动态对泛型类型参数的运行时类型的利用
- 原则40:使用动态接收匿名类型参数