前段时间写code的时候需要在类中定义一个常量的字符串,我就随手写了个const string = \"xxx\";。结果review别人的code时发现他们用的时static readonly,看起来效果差不多,那么究竟该用哪个呢?于是,我先把我们整个大工程里的code大概翻了翻,想看看大家都是怎么用的以及这两种有没有什么适用环境,结果是太混乱了,相同的情况下用这两个的都有。所以,我决定梳理一下这两个字段。
我觉得const与static readonly最大的区别在于,前者是静态常量,后者是动态常量。意思就是const在编译的时候就会对常量进行解析,并将所有常量出现的地方替换成其对应的初始化值。而动态常量static readonly的值则在运行时才拿到,编译的时候只是把它标识为只读,不会用它的值去替换,因此static readonly定义的常量的初始化可以比const稍微推迟一些。
为了更清楚得看到编译时获取值与运行时获取值的区别,这里有一个简单的例子。
我们写新建一个名为ConstStaticReadOnly的Console Application Project和一个名为MyClassConfig的Portable Class Library Project。
// ConstStaticReadOnly Project
namespace ConstStaticReadOnly
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(\"const value is: {0}\", MyClassConfig.ConstValue);
Console.WriteLine(\"static readonly value is: {0}\", MyClassConfig.ReadonlyValue);
}
}
}
// MyClassConfig Project
namespace ConstStaticReadOnly
{
public class MyClassConfig
{
public const string ConstValue = \"const\";
public static readonly string ReadonlyValue = \"readonly\";
}
}
编译运行我们可以看到下面的输出结果:
const value is: const
static readonly value is: readonly
下面我们修改一下两个常量的值 (后面都加上value),然后只把MyClassConfig重新编译一遍,并将生成的dll拷贝到ConstStaticReadOnly的build目录下替换原来的那个dll,再运行ConstStaticReadOnly则可以得到下面的输出:
const value is: const
static readonly value is: readonly value
从运行结果可以看到,只有static readonly那个值变了,const还是原来的值。这是因为我们没有重新build ConstStaticReadOnly工程,而它里面用到的ConstValue值早在上次build的时候就已经被替换成了\"const\"。那么,怎么才能把ConstStaticReadOnly里面的值变成最新的呢?很简单,在ConstValue值修改以后,重新build ConstStaticReadOnly。
这样一下子就可以看到在这里使用const的缺点了,如果我们的MyClassConfig被其他100个工程引用的话,每次修改MyClassConfig后一定要重新build这100个工程,不然的话这些工程里的const值就不会更新。
当然上面的例子并不是说const不好或者我们不要用const,只是说有些情况不适合用const,而且const也有自身的优点,如编译时就被解析从而免去了运行时的一些调用,既可以声明在类中也可以声明在函数体内等。
下面我们就来分析一下两者分别适用的情况。
(1)对于我们非常确定不会改变的常量,(这里的改变不是指运行时试图重新赋值来改变,而是指code中写的那个值被修改)例如:const int CM_IN_A_METER = 100;
(2)在函数体内声明的常量,例如:
void func()
{
const double PI = 3.14;
// use PI to do some calculation
}
(3)用于attribute里,例如
public static class Text
{
public const string ConstDescription = \"This can be used.\";
public readonly static string ReadonlyDescription = \"Cannot be used.\";
}
public class Fun
{
// You should add using System.ServiceModel.Description (System.ServiceModel.dll);
// and using System.ComponentModel (System.dll);
[Description(Text.ConstDescription)]
public int BarThatBuilds { get; set; }
[Description(Text.ReadOnlyDescription)]
public int BarThatDoesNotBuild { get; set; }
}
在attribute里面只能使用const常量,使用static readonly会出现编译错误。
Error 1 \'ConstStaticReadOnly.Text\' does not contain a definition for \'ReadOnlyDescription\'
(4)当你需要implicit conversion时
下面是stackoverflow上有人提供的一个例子,采用const与static readonly得到的结果会不一样。
const int y = 42;
static void Main()
{
short x = 42;
Console.WriteLine(x.Equals(y)); // True
}
static readonly int y = 42;
static void Main()
{
short x = 42;
Console.WriteLine(x.Equals(y)); // False
}
The reason is that the method x.Equals
has two overloads, one that takes in a short (System.Int16) and one that takes an object (System.Object). Now the question is whether one or both apply with my y argument.
对于const修饰的int常量情况,存在implicit conversion from int to short,这样比较的时候就使用了short版本的Equals;而static readonly修饰的int则不具有隐士转换的功能,比较的时候使用的object的Equals,如果你认为这种情况下他们应该相等,则可以在比较的时候进行显示转换,如x.Equals((short)y)。
(1)需要根据config文件里的值来初始化的
为了方便管理常量,我们通常会把一个project或者solution里的所有常量集中起来,采用config文件进行配置。这样不仅便于管理、修改和维护,而且可以在不同的环境下使用不同的config文件来初始化code里的那些常量。const修饰的常量必须在声明的时候就初始化在code里,肯定是做不到这一点的,所以可以采用static readonly来声明这些常量,然后在构造函数里load config文件,对所有相应的常量进行初始化。
(2)可能会发生变化的常量
其实(1)也可以看做是这一类,只是我觉得(1)比较常用,而且像(1)那样对常量进行集中管理是一种很好的习惯,所以才单独提出来了。下面来对可能发生变化的常量举一个例子,
class MyMathLib
{
private static readonly PI = 3.14;
}
为什么说PI是一个可能会变得常量呢?因为不同情况下你的工程对精度的要求可能不一样,某天如果突然间发现只保留两位小数时精度不够时,可能就会把它改成3.14159了。另外,这里的PI跟上面函数体内需要用到的PI必须用const并不矛盾,虽然函数体内的PI也可能会改变,但是并不要紧,因为它已经在函数体内了,改变后肯定会同时编译PI常量和那个函数。
(3)需要new操作符初始化的const一般用于修饰值类型或者string(注意string是引用类型)。因为引用类型(除了string)是要通过new关键字来初始化的,而const声明的常量是不能用new来初始化的,所以如果你一定要用const来修饰一个引用类型(string除外)的常量,请初始化为null。例如,Fun f = new Fun();会引起下面的编译错误:
Error 1 A const field of a reference type other than string can only be initialized with null.
所以,如果你要将引用类型的非空值定义为常量,你需要使用static readonly,
private static readonly List test = new List {1, 2, 3};
(4)关于private与public
类中static readonly修饰的常量应该用private还是public呢?如果用private,那客户端那边就不能直接访问了,所以就定义成public?对于一般的值类型或者string,定义成public static readonly当然没问题,这也是我们常用的。
可是对下面一种情况可能会有问题:
// ConstStaticReadOnly Project
namespace ConstStaticReadOnly
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(\"x={0}, y={1}\", MyClassConfig.ReadonlyPoint.x, MyClassConfig.ReadonlyPoint.y);
// MyClassConfig.ReadonlyPoint = new Point() is not allowed
// We cannot change the reference of ReadonlyPoint
// But we can change the fields in ReadonlyPoint
MyClassConfig.ReadonlyPoint.x = 3;
MyClassConfig.ReadonlyPoint.y = 4;
Console.WriteLine(\"x={0}, y={1}\", MyClassConfig.ReadonlyPoint.x, MyClassConfig.ReadonlyPoint.y);
}
}
}
// MyClassConfig Project
namespace ConstStaticReadOnly
{
public class Point
{
public int x;
public int y;
public Point(int a, int b)
{
x = a;
y = b;
}
}
public class MyClassConfig
{
public static readonly Point ReadonlyPoint = new Point(1, 2);
}
}
输出结果:
x=1, y=2
x=3, y=4
我们的本意应该是让ReadonlyPoint不能被外界改变,现在看来上面的static readonly并没有达到这个效果。这是因为static readonly修饰的常量只能保证reference不能变,也就是不能对ReadonlyPoint进行重新赋值,但是ReadonlyPoint引用的那个Point里面的值是可以被改变的,这叫mutable reference types。
所以在用FxCop 对代码进行分析时,会出现Do not declare read only mutable reference types的warning。也就是说上面那样用public static readonly修饰的ReadonlyPoint并不是安全的,下面有一种解决方案:
把ReadonlyPoint声明为private或者protected,然后提供一个仅提供get函数的property来返回内部的ReadonlyPoint。
protected static readonly Point readonlyPoint = new Point(1, 2);
public static Point ReadonlyPoint
{
get
{
return readonlyPoint;
}
}
(1)const常量在编译时解析;而static readonly常量在运行时解析。
(2)const常量必须在定义时初始化;而static readonly常量可以在定义时初始化,也可以在构造函数中初始化;
(3)非常确定不会改变的常量值可以用const,必须写在函数体内的常量需要用const,需要被attributes用到的常量应该用const。
(4)常量需要被客户端引用,且可能会改变,应该用static readonly。
参考文献: