一.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代码中,这也就是为什么常量不需要分配任何内存的原因.
      我们来验证一下上面的结论,首先我们定义一个常量:
 public class MathHelper
{
public const float PI= 3.14159F;
}
调用:
 static void Main(string[] args)
{
float pi= MathHelper.PI;
}
我们查看生成的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变量的
定义:
  public class MathHelper
{
public readonly float PI;
}
访问:
 
2.readonly出了构造函数,对于这个实例而言就不可改变,因此下面的写法也是错误的
     既然,我们强调"出了构造函数",那是不是意味着,我们在构建函数内部,可以一次或多次改变它的值?为了验证我们的猜想,我们对MathHelper改造如下:
 public class MathHelper
{
public MathHelper()
{
this.PI = 3.15F;
this.PI = 3.14F;
}
public readonly float PI;
}
调用代码:
 static void Main(string[] args)
{
MathHelper m = new MathHelper();
Console.WriteLine(m.PI);
}
输出结果:
从以上的结果,我们可以看出,在构造函数中可以对readonly变量多次赋值,但一旦出了构建函数则是只读的.
 
3.有了第2点的支撑,下面我们可以验证readonly是实例的(不可变的第一种情况)这一结论,我们现在来验证这个结论.
   我们改造MathHelper如下:
 public class MathHelper
{
public MathHelper(float pi)
{
this.PI = pi;
}
public readonly float PI;
}
调用如下:
 static void Main(string[] args)
{
MathHelper m1 = new MathHelper(3.14F);
Console.WriteLine(m1.PI); MathHelper m2 = new MathHelper(3.15F);
Console.WriteLine(m2.PI); Console.Read();
}
输出结果:
我们实例化了两个不同的MathHelper,给PI赋予了不同的值,PI的值属于不同的实例,这也就验证了我们的结论.
 
4.readonly的内联写法
那有的童鞋说了,我还用过这样的写法,这说明了readonly可以在构建方法外赋值.如下所示:
 public class MathHelper
{
public readonly float PI=3.15F;
}
     其实,这是一种内联写法,是C#的一种语法糖,只是一种语法上的简化,实际它们也是在构造方法中进行初始化的.C#允许使用这种简化的内联初始化语法来初始化类的常量、read/write字段和readonly字段。
 
5.readonly赋值的第二种情况:如果我用static修饰readonly会发生什么?
     前面讲const时,我们说过const是静态的,这种静态不可以显式指定,因此在const前加static会导致编译器编译失败.那我们把static修饰readonly会发生什么样的结果?
     首先,我们确定,静态的是属于类的,此时的readonly我们不能通过构造函数来指定.
 public class MathHelper
{
public static readonly float PI=3.14F;
}
调用:
 static void Main(string[] args)
{
Console.WriteLine(MathHelper.PI);
Console.Read();
}
结果与我们预期的一致:
 
但我们的疑问不会就此打住:既然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无异,但是对于引用类型,我们需要谨慎对待,不可想当然,下面我们通过实验来得出结论:
 public class Alphabet
{
public static readonly Char[] Letters = new Char[] {'A','B','C','D','E','F' };
}
调用:
 static void Main(string[] args)
{
Alphabet.Letters[] = 'a';
Alphabet.Letters[] = 'b';
Alphabet.Letters[] = 'c';
Alphabet.Letters[] = 'd';
Alphabet.Letters[] = 'e';
Alphabet.Letters[] = 'f';
Console.WriteLine(Alphabet.Letters.Length);
Console.Read();
}
可赋值!!!
输出结果如下:
 
现在,我们给它赋予一个新的对象:
不可赋值!!!
看到这里你是不是心里有答案了?
 
结论:对于引用类型而言,我们可以赋值,而不可以赋予一个新的对象,因为这里不变的是引用,而不是引用的对象.
 

四:总结

     到此,我们的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

C#夯实基础系列之const与readonly的更多相关文章

  1. 【C++自我精讲】基础系列二 const

    [C++自我精讲]基础系列二 const 0 前言 分三部分:const用法.const和#define比较.const作用. 1 const用法 const常量:const可以用来定义常量,不可改变 ...

  2. 夯实基础系列四:Linux 知识总结

    前言 前三节内容传送门: 夯实基础系列一:Java 基础总结 夯实基础系列二:网络知识总结 夯实基础系列三:数据库知识总结 现在很多公司项目部署都使用的是 Linux 服务器,互联网公司更是如此.对于 ...

  3. 【Unity|C#】基础篇(6)——const、readonly、static readonly

    [学习资料] <C#图解教程>(第6章):https://www.cnblogs.com/moonache/p/7687551.html 电子书下载:https://pan.baidu.c ...

  4. JavaScript夯实基础系列(四):原型

      在JavaScript中有六种数据类型:number.string.boolean.null.undefined以及对象,ES6加入了一种新的数据类型symbol.其中对象称为引用类型,其他数据类 ...

  5. JavaScript夯实基础系列(三):this

      在JavaScript中,函数的每次调用都会拥有一个执行上下文,通过this关键字指向该上下文.函数中的代码在函数定义时不会执行,只有在函数被调用时才执行.函数调用的方式有四种:作为函数调用.作为 ...

  6. JavaScript夯实基础系列(二):闭包

      在JavaScript中函数是一等公民.所谓一等公民是指函数跟其他对象一样,很普通,可以进行把函数存在数组中.作为参数传递.赋值给变量等操作.当函数作为另一个函数的返回值在外部调用时,跟该函数在函 ...

  7. JavaScript夯实基础系列(一):词法作用域

      作用域是一组规则,规定了引擎如何通过标识符名称来查询一个变量.作用域模型有两种:词法作用域和动态作用域.词法作用域是在编写时就已经确定的:通过阅读包含变量定义的数行源码就能知道变量的作用域.Jav ...

  8. 夯实基础系列一:Java 基础总结

    前言 大学期间接触 Java 的时间也不短了,不论学习还是实习,都让我发觉基础的重要性.互联网发展太快了,各种框架各种技术更新迭代的速度非常快,可能你刚好掌握了一门技术的应用,它却已经走在淘汰的边缘了 ...

  9. 【PHP夯实基础系列】PHP日期,文件系统等知识点

    1. PHP时间 1)strtotime() //日期转成时间戳 2) date()//时间戳变成日期 <?php date_default_timezone_set("PRC&quo ...

随机推荐

  1. BBR拥塞控制算法

    BBR拥塞控制算法是Google最新研发的单边TCP拥塞控制算法Linux 内核4.9 已引入这个BBR算法,本人在CAC测试Ubuntu 14.04 安装Linux 4.9内核,延迟优化效果和TCP ...

  2. IO(六)--- 编码和解码

    编码: 把看得懂的字符变成看不懂码值这个过程我们称作为编码. 解码: 把码值查找对应的字符,我们把这个过程称作为解码. 注意: 以后编码与解码一般我们都使用统一的码表.否则非常容易出乱码. 常用码表: ...

  3. 大流量网站性能优化:一步一步打造一个适合自己的BigRender插件

    BigRender 当一个网站越来越庞大,加载速度越来越慢的时候,开发者们不得不对其进行优化,谁愿意访问一个需要等待 10 秒,20 秒才能出现的网页呢? 常见的也是相对简单易行的一个优化方案是 图片 ...

  4. 微信快速开发框架(九)-- V3.0发布,代码已更新至Github 新增微店功能

    版本内容 1.修正了缺少对Event.View的支持 2.增加了用户UnionID 3.新增微信小店功能 4.多客服功能 5.单元测试 什么是UnionID 我们知道,每个用户针对一个微信公众账号都有 ...

  5. Visual Studio 2015 Pre Secondary Installer 在哪里

    安装vs2015 pre后,会自动打开Secondary Installer, 用于Cross Platform的移动开发框架,包括Cordova插件.若安装失败,启动程序位置: "D:\P ...

  6. 安装MySQL的时候遇到的错误

    这里我安装的是MySQL5.6 我遇到的错误有 (1)Warning: Bison executable not found in PATH 解决办法: yum install bison 原文摘自: ...

  7. 时间戳 时区 java mysql

    当一个时间 比如2016年5月6日,生成时间戳.这个运算是与时区有关的.首先得确认这个时间是哪个时区的,然后转换成utc时区的时间.再减去1970,得到的秒数,就是时间戳. 时间戳是个一定的值,他与时 ...

  8. Python Day3

    一.set集合 集合是一个无序的,不重复的数据组合,它的主要作用如下: 去重,把一个列表变成集合,就自动去重了 关系测试,测试两组数据之前的交集.差集.并集等关系 # 创建数值集合 list_1 = ...

  9. [Fluent NHibernate]第一个程序

    目录 写在前面 Fluent Nhibernate简介 基本配置 总结 写在前面 在耗时两月,NHibernate系列出炉这篇文章中,很多园友说了Fluent Nhibernate的东东,也激起我的兴 ...

  10. JMS开发步骤和持久化/非持久化Topic消息

    ------------------------------------------------ 开发一个JMS的基本步骤如下: 1.创建一个JMS connection factory 2.通过co ...