C#庖丁解牛之const与readonly

一.const与readonly的争议

你一定写过const,也一定用过readonly,但说起两者的区别,并说出何时用const,何时用readonly,你是否能清晰有条理地说出个一二三?

const与readonly之所以有如此争议,是因为彼此都存在"不可改变"这一特性,对于二者而言,我们需要关心的是,什么时候开始不可变?什么是不可改变的?这就引出了我们下面要讨论的话题.

二.什么时候开始不可变?

我们先抛出结论.

const在程序运行的任何时候都是不可变的,无论什么时候开始,什么时候结束,它的值是固化在代码中的,我们称之为编译期常量;

      readonly在某个具体实例第一次初始时指定它的值(出了构造函数后,对于这个实例而言,它就不能改变)或者是作为静态成员在运行时加载它的值,我们称之为运行时常量.

我们先谈const:

      1.const由于其值从不变化,我们称之为常量,常量总是静态的,因此const是天然static的,我们不能再用static修饰const.如下图所示:

正确的定义应该是const float PI=3.14159F;

2.const既然是静态的,因此它属于整个类,而不属于某个实例,我们可以直接通过类名来调用,如下所示:

3.由于常量的值是直接嵌入代码的,因此在运行时不需要为常量分配任何内存,也不能获取常量的地址,也不能以传引用的方式传递常量.

什么叫直接嵌入代码?即:在编译的过程中,编译器首先将常量值保存到程序集元数据中,在引用常量的地方,编译器将提取这个常量值并嵌入生成的IL代码中,这也就是为什么常量不需要分配任何内存的原因.

我们来验证一下上面的结论,首先我们定义一个常量:

1 public class MathHelper
2 {
3     public const float PI= 3.14159F;
4 }

调用:

1 static void Main(string[] args)
2 {
3      float pi= MathHelper.PI;
4 }

我们查看生成的IL代码,如下:

标红的那一行,即是将PI的值直接嵌入代码之中.理解这一点不难,但是这种写法会带来潜在的问题:const不能支持很好支持程序集的跨版本.为了说明这个问题,我们需要对我们的代码进行如下的改造:

第一步:我们将MathHelper单独放到一个项目中,并生成一个单独的程序集(程序集版本:1.0).

第二步:我们编译应用程序为exe文件,采用上面的方法来查看IL代码,我们看到const的值仍然嵌入了代码之中.

第三步:我们修改PI的值为3.14,重新编译MathHelper,生成一个单独的程序集(程序集版本:2.0).

第四步:因为我们只是重新编译了MathHelper所在的程序集,没有重新编译exe文件,我们查看exe的IL代码,发现嵌入代码的值仍为3.14159.

也就是在跨程序集的引用中,当改变了常量时,除非重新编译所有引用了常量的程序集,否则改变不能体现在引用当中.

虽然有了这样的bug隐患,也不是说const就一无是处,由于const在程序中不占用内存,所以它的速度非常之快,于是我们在设计程序时,如果一个值从不变化,我们可以将其定义常量来寻求速度上的效率上的提升.比如我们程序需要国际化的时候,简体中文的编码为2052,美国英语的编码为1033,我们可以将它们定义为常量.

另外,我们说过常量是没有地址的,因而不能以传引用的方式传递常量,即下面的写法是错误的:

说完const,我们来说readonly

1.readonly是实例的,因此通过类名是不可直接访问readonly变量的

定义:

1  public class MathHelper
2 {
3       public readonly float PI;
4 }

访问:

2.readonly出了构造函数,对于这个实例而言就不可改变,因此下面的写法也是错误的

既然,我们强调"出了构造函数",那是不是意味着,我们在构建函数内部,可以一次或多次改变它的值?为了验证我们的猜想,我们对MathHelper改造如下:

1 public class MathHelper
2 {
3       public MathHelper()
4       {
5             this.PI = 3.15F;
6             this.PI = 3.14F;
7        }
8       public readonly float PI;
9 }

调用代码:

1 static void Main(string[] args)
2 {
3       MathHelper m = new MathHelper();
4        Console.WriteLine(m.PI);
5 }

输出结果:

从以上的结果,我们可以看出,在构造函数中可以对readonly变量多次赋值,但一旦出了构建函数则是只读的.

3.有了第2点的支撑,下面我们可以验证readonly是实例的(不可变的第一种情况)这一结论,我们现在来验证这个结论.

我们改造MathHelper如下:

1 public class MathHelper
2 {
3    public MathHelper(float pi)
4    {
5       this.PI = pi;
6    }
7    public readonly float PI;
8 }

调用如下:

 1 static void Main(string[] args)
 2 {
 3      MathHelper m1 = new MathHelper(3.14F);
 4      Console.WriteLine(m1.PI);
 5
 6      MathHelper m2 = new MathHelper(3.15F);
 7      Console.WriteLine(m2.PI);
 8
 9      Console.Read();
10 }

输出结果:

我们实例化了两个不同的MathHelper,给PI赋予了不同的值,PI的值属于不同的实例,这也就验证了我们的结论.

4.readonly的内联写法

那有的童鞋说了,我还用过这样的写法,这说明了readonly可以在构建方法外赋值.如下所示:

1 public class MathHelper
2 {
3     public readonly float PI=3.15F;
4 }

其实,这是一种内联写法,是C#的一种语法糖,只是一种语法上的简化,实际它们也是在构造方法中进行初始化的.C#允许使用这种简化的内联初始化语法来初始化类的常量、read/write字段和readonly字段。

5.readonly赋值的第二种情况:如果我用static修饰readonly会发生什么?

前面讲const时,我们说过const是静态的,这种静态不可以显式指定,因此在const前加static会导致编译器编译失败.那我们把static修饰readonly会发生什么样的结果?

首先,我们确定,静态的是属于类的,此时的readonly我们不能通过构造函数来指定.

1 public class MathHelper
2 {
3     public static readonly float PI=3.14F;
4 }

调用:

1 static void Main(string[] args)
2 {
3      Console.WriteLine(MathHelper.PI);
4      Console.Read();
5 }

结果与我们预期的一致:

但我们的疑问不会就此打住:既然static readonly也是属于类的,而且它的值也不能通过构造函数来赋值,那么编译器会像const一样把它的值写入IL代码中么?我们反编译其IL代码如下:

可以看到,这里并没有将值嵌入到代码当中.

因此,我们可以大胆地预测,这种写法不会造成支持程序集的跨版本问题.这里就不写验证的过程,留给各位读者朋友自行探索.

既然没有嵌入代码中,那么在程序运行的时候,它的值是在什么时候分配内存的呢?

我们引用《CLR via C#(第4版)》中的一句话来说明这个问题:对于静态字段,其动态内存是在类中分配的,而类是在类型加载到AppDomain时创建的,那么,什么时候将类型加载到AppDomain中呢?答案是:通常是在引用了该类型的任何方法首次进行JIT编译的时候.而对于前面第3点中的实例字段来说,其动态内存是在构造类型的实例时分配的.

三.什么是不可变的?

前面我们花了大量的篇幅说明const与readonly的变量什么时候才开始不可变,有的从一开始就不可变,有的是第一次加载的时候不可变,有的是出了构造函数后不可变,但是我们有一个十分关键的问题没有弄清楚:什么东西不可变?也许童鞋们很疑惑,值不可变呗!这话不完全对.

要想理解这个问题,我们需要明白const与readonly修饰的对象,也就是我们不变的内容.

const可以修饰基元类型:Boolean、Char、Byte、SByte、Int16、UInt16、Int32、UInt32、Int64、UInt64、Single、Double、Decimal和String。也可以修改类class,但要把值设置为null。不可以修饰struct,因为struct是值类型,不可以为null.

对于基元类型来说,值是存储在栈上的,因此我们可以认为不变的是值本身,这里string是一个特殊的引用类型,这里它也存在值类型的特征,因此也可以认为它不变的是值本身.

对于readonly而言,readonly可以修饰任何类型.对于基元类型而言,我们可以认为它与const无异,但是对于引用类型,我们需要谨慎对待,不可想当然,下面我们通过实验来得出结论:

1 public class Alphabet
2 {
3         public static readonly Char[] Letters = new Char[] {‘A‘,‘B‘,‘C‘,‘D‘,‘E‘,‘F‘ };
4 }

调用:

 1 static void Main(string[] args)
 2 {
 3      Alphabet.Letters[0] = ‘a‘;
 4      Alphabet.Letters[0] = ‘b‘;
 5      Alphabet.Letters[0] = ‘c‘;
 6      Alphabet.Letters[0] = ‘d‘;
 7      Alphabet.Letters[0] = ‘e‘;
 8      Alphabet.Letters[0] = ‘f‘;
 9      Console.WriteLine(Alphabet.Letters.Length);
10      Console.Read();
11  }

可赋值!!!

输出结果如下:

现在,我们给它赋予一个新的对象:

不可赋值!!!

看到这里你是不是心里有答案了?

结论:对于引用类型而言,我们可以赋值,而不可以赋予一个新的对象,因为这里不变的是引用,而不是引用的对象.

四:总结

到此,我们的const与readonly的庖丁解牛式的解析也就告一段落了,说了这么多,我们其实也就是想说明以下2点:

1.const任何时候都不变,比readonly快,但不能解决跨版本程序集问题,readonly静态时在第一次JIT编译后不变,实例时在出了实例的构造函数后不可变.

    2.const修饰基元类型,不变的是值;readonly修饰值类型时,其值不变,修改引用类型时,其引用不变.

以上.

参考文档:

《CLR via C#(第4版)》

《Effice C#:改进C#代码的50个行之有效的办法》

《编写高质量代码:改善C#程序的157个建议》

博客:http://www.cnblogs.com/royenhome/archive/2010/05/22/1741592.html

时间: 2024-12-19 13:55:07

C#庖丁解牛之const与readonly的相关文章

[c#] const 与 readonly

c# 中 const 与 readonly 关键字看似相同,实则不同.重点在于确定值的时间. const const 很简单,就是一个常量,不可以被 static 修饰,因为被 const 修饰的字段自动成为静态字段,其值是在编译时可以确定的. readonly readonly 可以修饰实例字段(不被 static 修饰的字段),也可以修饰静态字段(被 static 修饰的字段).意指为"只读".其值确定的时间在类的构造函数中. 总结 关键字 确定值的时机 const 编译时 rea

const和readonly

const:常量,编译时即需要确定值 readonly:只读变量,运行时确定值 1 class ConstReadonlyTest 2 { 3 //const String str;//错误:常量字段定义时需要指定初始值 4 //const Object obj = new Object();//错误:常量字段不能使用new初始化,表达式必须是常量(字符串可以) 5 //const Object obj = new StructTest();//错误:表达式必须是常量不能使用new初始化(即使是

const,readonly,static

1.const 表示的是常量(constant),始终不会发生改变,在编译时就确定了.所以类中定义一个常量可以被类访问也可以被类的实例访问.定义时就不能和static一起用.如果用了也是没有作用的,所以语法就规定其是无效的.在声明时同时初期化. private const string Name="abc"; 2.readonly 值在运行期决定,有一次确定其值的机会.其是可以被类访问还是类的实例访问,就看static.可以和static一起使用,这与一般数据成员一样.可以在两类地方赋

C#基础知识七之const和readonly关键字

前言 不知道大家对const和readonly关键字两者的区别了解多少,之前真不是很清楚,如果你也不是很清楚的话,那就一起来探讨吧!探讨之前我们先来了解静态变量和动态变量. 静态变量 所谓静态变量就是在编译期间会对变量进行解析,再讲常量的值替换成初始化的值.定义时必须初始化. 动态变量 所谓动态变量就是编译期间会将变量标示只读常量,而不用常量的值代替,定义时可以不初始化,可以延迟到构造函数. const和readonly

const与readonly常量

const与readonly都是用来定义常量,但是它们有什么区别呢? 下面我们来简要的说明一下: const修饰的常量是编译时常量,如:public const String PI=3.1415;什么是编译时常量,通俗来讲就是指你在声明时,必须要进行赋值(也就是初始化),如果不赋值,编译时会出现说“常量字段要求提供一个值”的异常. readonly修饰的是运行时常量,可以在声明中赋值,也可以在构造函数中赋值(注意只能在这两个地方赋值). http://www.cnblogs.com/royenh

.net 中const 与readonly的区别???

const和readonly都是只读的.但是const修饰的变量必须在声明时赋值.readonly修饰的变量可以在声明时或者构造函数里赋值.private const double pi=3.14;class A { private readonly int a; public A(int v) { a=v; } }

const和readonly你真的懂吗?

第二遍文章我打算把const和readonly的区别拿出来讲下,因为写代码这么久我都还没搞清楚这两者的区别,实在有点惭愧,所以这一次我打算搞清楚它. 定义 来看看MSDN的解释: readonly:readonly关键字是可以在字段上使用的修饰符.当字段声明包括readonly修饰符时,该声明引入的字段赋值只能作为声明的一部分,或者出现在同一类的构造函数中. const:使用 const 关键字来声明某个常量字段或常量局部变量. 常量字段和常量局部变量不是变量并且不能修改. 太多理论的讲解有些人

C# const 和readonly

class Program { public Program(int y) { this.y = y; } public const int x = 123;//默认是静态的值,在声明的时候必须初始化,引用类型只有string和null才能声明为const public readonly int y;//默认是实例成员,一般在声明的时候初始化或者是在类的构造函数里初始化. static void Main(string[] args) { Program pg = new Program(333

C#基础 const和readonly关键字

静态常量 所谓静态常量就是在编译期间会对变量进行解析,再将常量的值替换成初始化的值.动态常量 所谓动态常量就是编译期间会将变量标记只读常量,而不用常量的值代替,这样在声明时可以不初始化,可以延迟到构造函数初始化. const和readonly 区别 const修饰的常量是上述中的第一种,即静态常量,而readonly是上述中第二种即动态常量.他们的区别可以从静态常量和动态常量的特性来说明:const修饰的常量在声明时必须初始化值:readonly修饰的常量可以不初始化值,且可以延迟到构造函数.c