C#基础:基于const与readonly的深入研究
•readonly和const都是用来标识常量的[1]。
•const可用于修饰class的field或者一个局部变量(local variable);而readonly仅仅用于修饰class的field。
•const常量的值必定在编译时就已明确并且恒定的;而readonly常量却有一点不同,那就是其值可以在运行时编译,当然,它也必须遵守作为常量的约束,那就是值必须恒定不变。
•const常量必须在声明的同时对其进行赋值,并且确保该值在编译时可确定并恒定;而readonly常量则可以根据情况选择在声明的同时对其赋予一个编译时确定并恒定的值,或者将其值的初始化工作交给实例构造函数(instant constructor)完成。如:public readonly string m_now = datetime.now.tostring();,m_now会随着运行时实际情况变化而变化。
•const常量属于类级别(class level)而不是实例对象级别(instant object level),并且它不能跟static结合一起使用,该常量的值将由整个类的所有实例对象共同分享(详细论述参见后面的remark区域)。
•readonly常量既可以是类级别也可以是实例对象级别的,这取决于它的声明以及初始化工作怎么实施。readonly可以与static结合使用,用于指定该常量属于类级别,并且把初始化工作交由静态构造函数(static constructor)完成(有关如何把readonly常量声明为类级别或实例对象级别的论述清参见后面的remark区域)。
•能被const修饰声明为常量的类型必须是以下的基元类型(primitive type):sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, float, bool, decimal, string。
•object, 数组(array)和结构(struct)不能被声明为const常量。
•一般情况下,引用类型是不能被声明为const常量的,不过有一个例外:string。该引用类型const常量的值可以有两种情况,string或null。其实,string虽然是引用类型,但是.net却对它特别处理,这种处理叫做字符串恒定性(immutable),使得string的值具有只读特性。有关字符串恒定性的内容,可以参考《microsoft .net框架程序设计(修订版)》。
examples:
code
using system;
publicclass order
{
public order()
{
guid guid = guid.newguid();
id = guid.tostring("d");
}
// 对于每一份订单,其订单序号都是实时确定的常量。
style="color: #008000;">
publicreadonlystring id;
publicoverridestring tostring()
{
return"order id: "+ id;
}
}
explaintion:
•如果结合数据库使用,id field通常都会都会与某个表的主健(primary key)关联起来,如orders表的orderid。
•数据库的主健通常采用以下三种方式:
•自动递增值。你可以通过把datacolumn.autoincrement设定为true值来激活自动递增特性。
•唯一名称。这个是使用自己定义的算法来生成一个唯一序列号。
•guid(全局唯一标识符)。你可以通过system.guid结构来生成guid,如上例。
code
using system;
class customer
{
public customer(string name, int kind)
{
m_name = name;
m_kind = kind;
}
publicconstint normal =0;
publicconstint vip =1;
publicconstint super_vip =2;
privatestring m_name;
publicstring name
{
get { return m_name; }
}
privatereadonlyint m_kind;
publicint kind
{
get { return m_kind; }
}
publicoverridestring tostring()
{
if(m_kind == super_vip)
return"name: "+ m_name +"[supervip]";
elseif(m_kind == vip)
return"name: "+ m_name +"[vip]";
else
return"name: "+ m_name +"[normal]";
}
}
•一般情况下,如果你需要声明的常量是普遍公认的并作为单个使用,例如圆周率,黄金分割比例等。你可以考虑使用const常量,如:public const double pi = 3.1415926;。如果你需要声明常量,不过这个常量会随着实际的运行情况而决定,那么,readonly常量将会是一个不错的选择,例如上面第一个例子的订单号order.id。
•另外,如果要表示对象内部的默认值的话,而这类值通常是常量性质的,那么也可以考虑const。更多时候我们对源代码进行重构时(使用replace magic number with symbolic constant),要去除魔数(magic number)的影响都会借助于const的这种特性。
•对于readonly和const所修饰的变量究竟是属于类级别的还是实例对象级别的问题,我们先看看如下代码:
code
using system;
namespace constantlab
{
class program
{
staticvoid main(string[] args)
{
constant c =new constant(3);
console.writeline("constint = "+ constant.constint.tostring());
console.writeline("readonlyint = "+ c.readonlyint.tostring());
console.writeline("instantreadonlyint = "+ c.instantreadonlyint.tostring());
console.writeline("staticreadonlyint = "+ constant.staticreadonlyint.tostring());
console.writeline("press any key to continue");
console.readline();
}
}
class constant
{
public constant(int instantreadonlyint)
{
instantreadonlyint = instantreadonlyint;
}
publicconstint constint =0;
publicreadonlyint readonlyint =1;
publicreadonlyint instantreadonlyint;
publicstaticreadonlyint staticreadonlyint =4;
}
}
•使用visual c#在main()里面使用intellisence插入constant的相关field的时候,发现readonlyint和instantreadonlyint需要指定constant的实例对象;而constint和staticreadonlyint却要指定constant class(参见上面代码)。可见,用const或者static readonly修饰的常量是属于类级别的;而readonly修饰的,无论是直接通过赋值来初始化或者在实例构造函数里初始化,都属于实例对象级别。
•一般情况下,如果你需要表达一组相关的编译时确定常量,你可以考虑使用枚举类型(enum),而不是把多个const常量直接嵌入到class中作为field,不过这两种方式没有绝对的孰优孰劣之分。
code
using system;
enum customerkind
{
supervip,
vip,
normal
}
class customer
{
public customer(string name, customerkind kind)
{
m_name = name;
m_kind = kind;
}
privatestring m_name;
publicstring name
{
get { return m_name; }
}
private customerkind m_kind;
public customerkind kind
{
get { return m_kind; }
}
publicoverridestring tostring()
{
return"name: "+ m_name +"["+ m_kind.tostring() +"]";
}
}
然而,当这种结合使用枚举和条件判断的代码阻碍了你进行更灵活的扩展,并有可能导致日后的维护成本增加,你可以代之以多态,使用replace conditional with polymorphism来对代码进行重构.
comments:
•readonly field准确来说应该翻译成为“只读域”,这里是为了统一翻译用语才将它和const两者所修饰的量都说成“常量”,希望没有引起误会。
工作原理
readonly为运行时常量,程序运行时进行赋值,赋值完成后便无法更改,因此也有人称其为只读变量。
const为编译时常量,程序编译时将对常量值进行解析,并将所有常量引用替换为相应值。
下面声明两个常量:
public static readonly int a = 2; //a为运行时常量
public const int b = 3; //b为编译时常量
下面的表达式:
int c = a + b;
经过编译后与下面的形式等价:
int c = a + 3;
可以看到,其中的const常量b被替换成字面量3,而readonly常量a则保持引用方式。
声明及初始化
readonly常量只能声明为类字段,支持实例类型或静态类型,可以在声明的同时初始化或者在构造函数中进行初始化,初始化完成后便无法更改。
const常量除了可以声明为类字段之外,还可以声明为方法中的局部常量,默认为静态类型(无需用static修饰,否则将导致编译错误),但必须在声明的同时完成初始化。
数据类型支持
由于const常量在编译时将被替换为字面量,使得其取值类型受到了一定限制。const常量只能被赋予数字(整数、浮点数)、字符串以及枚举类型。下面的代码无法通过编译:
public const datetime d = datetime.minvalue;
改成readonly就可以正常编译:
public readonly datetime d = datetime.minvalue;
可维护性
readonly以引用方式进行工作,某个常量更新后,所有引用该常量的地方均能得到更新后的值。
const的情况要稍稍复杂些,特别是跨程序集调用:
public class class1
{
public static readonly int a = 2; //a为运行时常量
public const int b = 3; //b为编译时常量
}
public class class2
{
public static int c = class1.a + class1.b; //变量c的值为a、b之和
}
console.writeline(class2.c); //输出"5"
假设class1与class2位于两个不同的程序集,现在更改class1中的常量值:
public class class1
{
public static readonly int a = 4; //a为运行时常量
public const int b = 5; //b为编译时常量
}
编译class1并部署(注意:这时并没有重新编译class2),再次查看变量c的值:
console.writeline(class2.c); //输出"7"
结果可能有点出乎意料,让我们来仔细观察变量c的赋值表达式:
public static int c = class1.a + class1.b;
编译后与下面的形式等价:
public static int c = class1.a + 3;
因此不管常量b的值如何变,对最终结果都不会产生影响。虽说重新编译class2即可解决这个问题,但至少让我们看到了const可能带来的维护问题。
性能比较
const直接以字面量形式参与运算,性能要略高于readonly,但对于一般应用而言,这种性能上的差别可以说是微乎其微。
适用场景
在下面两种情况下:
a.取值永久不变(比如圆周率、一天包含的小时数、地球的半径等)
b.对程序性能要求非常苛刻
可以使用const常量,除此之外的其他情况都应该优先采用readonly常量。
尽管你写了很多年的c#的代码,但是可能当别人问到你const与readonly的区别时候,还是会小小的愣一会吧~
笔者也是在看欧立奇版的《.net 程序员面试宝典》的时候,才发现自己长久以来竟然在弄不清出两者的情况下,混用了这么长的时间。的确,const与readonly 很像,都是将变量声明为只读,且在变量初始化后就不可改写。那么,const与readonly 这两个修饰符到底区别在什么地方呢?其实,这个牵扯出c#语言中两种不同的常量类型:静态常量(compile-time constants)和动态常量(runtime constants)。这两者具有不同的特性,错误的使用不仅会损失效率,而且还会造成错误。
首先先解释下什么是静态常量以及什么是动态常量。静态常量是指编译器在编译时候会对常量进行解析,并将常量的值替换成初始化的那个值。而动态常量的值则是在运行的那一刻才获得的,编译器编译期间将其标示为只读常量,而不用常量的值代替,这样动态常量不必在声明的时候就初始化,而可以延迟到构造函数中初始化。
当你大致了解上面的两个概念的时候,那么就可以来说明const与readonly了。const修饰的常量是上述中的第一种,即静态常量;而readonly则是第二种,即动态常量。那么区别可以通过静态常量与动态常量的特性来说明:
1)const修饰的常量在声明的时候必须初始化;readonly修饰的常量则可以延迟到构造函数初始化
2)const修饰的常量在编译期间就被解析,即常量值被替换成初始化的值;readonly修饰的常量则延迟到运行的时候
此外const常量既可以声明在类中也可以在函数体内,但是static readonly常量只能声明在类中。
可能通过上述纯概念性的讲解,对有些初学者有些晕乎。下面就一些例子来说明下:
using system;
class p
{
static readonly int a=b*10;
static readonly int b=10;
public static void main(string[] args)
{
console.writeline("a is {0},b is {1} ",a,b);
}
}
对于上述代码,输出结果是多少?很多人会认为是a is 100,b is 10吧!其实,正确的输出结果是a is 0,b is 10。好吧,如果改成下面的话:
using system;
class p
{
const int a=b*10;
const int b=10;
public static void main(string[] args)
{
console.writeline("a is {0},b is {1} ",a,b);
}
}
对于上述代码,输出结果又是多少呢?难道是a is 0,b is 10?其实又错了,这次正确的输出结果是a is 100,b is 10。
那么为什么是这样的呢?其实在上面说了,const是静态常量,所以在编译的时候就将a与b的值确定下来了(即b变量时10,而a=b*10=10*10=100),那么main函数中的输出当然是a is 100,b is 10啦。而static readonly则是动态常量,变量的值在编译期间不予以解析,所以开始都是默认值,像a与b都是int类型,故都是0。而在程序执行到a=b*10;所以a=0*10=0,程序接着执行到b=10这句时候,才会真正的b的初值10赋给b。如果,你还是不大清楚的话,我们可以借助于微软提供的ildasm工具,只需在vs 2008 command下输入ildasm就可以打开,如下所示:
分别打开上述两个代码编译后产生的可执行文件,如下图所示:
static readonly可执行程序的结构
const
可执行程序的结构
在上述两张图中都可以看到a与b常量,分别双击节点可以看出其中的差异:
static readonly修饰的常量a
const修饰的常量a
static readonly修饰的常量b
const修饰的常量b
从上图中可以看出,const修饰的常量在编译期间便已将a,b的字面值算出来了,而static readonly修饰的常量则未解析,所以在main函数中有以下的区别:
static readonly程序的main函数
const程序的main函数
从main函数中我们可以看出,const的那个程序的输出直接是100与10,而readonly在输出的时候确实p::a与p::b,即将a与b常量的值延迟到运行的时候才去确定,故输出是0与10。
那么对于静态常量以及动态常量还有什么特性呢?其实,静态常量只能被声明为简单的数据类型(int以及浮点型)、枚举、布尔或者字符串型,而动态常量则除了这些类型,还可以修饰一些对象类型。如datetime类型,如下:
//错误
const datetime time=new datetime();
//正确
static readonly datetime time=new datetime();
上述错误在于不能使用new关键字初始化一个静态常量,即便是一个值类型,因为new将会导致到运行时才能确定值,与静态变量编译时就确定字面值有悖。
欧书上最后给出了对静态常量与动态常量之间的比较,如下表所示: