[C#] const vs. static readonly

2185 查看

前段时间写code的时候需要在类中定义一个常量的字符串,我就随手写了个const string = "xxx";。结果review别人的code时发现他们用的时static readonly,看起来效果差不多,那么究竟该用哪个呢?于是,我先把我们整个大工程里的code大概翻了翻,想看看大家都是怎么用的以及这两种有没有什么适用环境,结果是太混乱了,相同的情况下用这两个的都有。所以,我决定梳理一下这两个字段。

1. const与static readonly的最主要区别

我觉得conststatic readonly最大的区别在于,前者是静态常量,后者是动态常量。意思就是const在编译的时候就会对常量进行解析,并将所有常量出现的地方替换成其对应的初始化值。而动态常量static readonly的值则在运行时才拿到,编译的时候只是把它标识为只读,不会用它的值去替换,因此static readonly定义的常量的初始化可以比const稍微推迟一些。

为了更清楚得看到编译时获取值与运行时获取值的区别,这里有一个简单的例子。
我们写新建一个名为ConstStaticReadOnlyConsole Application Project和一个名为MyClassConfigPortable 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拷贝到ConstStaticReadOnlybuild目录下替换原来的那个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上有人提供的一个例子,采用conststatic 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则不具有隐士转换的功能,比较的时候使用的objectEquals,如果你认为这种情况下他们应该相等,则可以在比较的时候进行显示转换,如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

参考文献:

  1. const (C# Reference)
  2. readonly (C# Reference)
  3. What is the difference between const and readonly?
  4. Static readonly vs const
  5. const和readonly区别
  6. Do not declare read only mutable reference types