[C#] const vs. static readonly
前段时间写code的时候需要在类中定义一个常量的字符串,我就随手写了个const string = "xxx";
。结果review别人的code时发现他们用的时static readonly
,看起来效果差不多,那么究竟该用哪个呢?于是,我先把我们整个大工程里的code大概翻了翻,想看看大家都是怎么用的以及这两种有没有什么适用环境,结果是太混乱了,相同的情况下用这两个的都有。所以,我决定梳理一下这两个字段。
1. const与static readonly的最主要区别
我觉得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
也有自身的优点,如编译时就被解析从而免去了运行时的一些调用,既可以声明在类中也可以声明在函数体内等。
下面我们就来分析一下两者分别适用的情况。
2. 什么时候用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)
。
3. 什么时候用static readonly
(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<int> test = new List<int> {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;
}
}
4. 小结
(1)const
常量在编译时解析;而static readonly
常量在运行时解析。
(2)const
常量必须在定义时初始化;而static readonly
常量可以在定义时初始化,也可以在构造函数中初始化;
(3)非常确定不会改变的常量值可以用const
,必须写在函数体内的常量需要用const
,需要被attributes
用到的常量应该用const
。
(4)常量需要被客户端引用,且可能会改变,应该用static readonly
。
参考文献: