[C#] const vs. static readonly

前段时间写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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • 注:这是第三遍读《C语言深度解剖》,想想好像自从大学开始就没读完过几本书,其中谭浩强的那本《C语言程序设计(第四版...
    HavenXie阅读 1,714评论 1 6
  • const 引用 const 引用是指向 const 对象的引用:const int ival = 1024;co...
    rogerwu1228阅读 617评论 0 1
  • (1)可以定义 const 常量 (2)const 可以修饰函数的参数、返回值. 详细内容: 1、什么是const...
    幽鬼09阅读 705评论 0 4
  • 雨过天晴云浪漫,薄雾随风卷。田野又增辉,黄土高坡、收麦骄阳赞。 一年一度粮仓满,种地多思变。大路去家乡,渭水长流、...
    木貞ma阅读 200评论 2 2
  • 亲爱的晋善: 新年快乐,今天是大年初一,农历新年的第一天,每一个月的第一天都是初一,中国人是相当重视这样的第一天,...
    丁爸阅读 606评论 0 51