Effective C# 原则2:为你的常量选择readonly而不是const(译)

转载 2007年09月26日 17:22:00
对于常量,C#里有两个不同的版本:运行时常量和编译时常量。
因为他们有不同的表现行为,所以当你使用不当时,将会损伤程序性能或者出现错误。
两害相权取其轻,当我们不得不选择一个的时候,我们宁可选择一个运行慢一点但正确的那一个,而不是运行快一点但有错误的那个。基于这个理由,你应该选择运行时常量而不是编译时常量(译注:这里隐藏的说明了编译时常量效率更高,但可能会有错误)。
编译时常量更快更直接,但在可维护性上远不及运行时常量。保留编译时常量是为了满足那些对性能要求克刻,且随着程序运行时间的过去,其值永远不发生改变的常量使用的(译注:这说明编译时常量是可以不被C#采用的,但考虑到性能问题,还是做了保留)。
你可以用关键字readonly来声明(declare)一个运行时常量,编译时常量是用关键字const声明的。
//Compile time constant:
public cocnst int _Millennium = 2000;
//Runtime constant:
public static readonly int _ThisYear = 2007;//(译注:原文为2004)
编译时常量与运行时常量不同之处表现在如何对他们的访问上。
一个编译时常量会被目标代码中的值直接取代。下面的代码:
if(myDateTime.Year == _Millennium)
会与下面写的代码编译成完全相同的IL代码:
if(myDateTime.Year == 2000)

运行时常量的值是在运行时确定的。当你引用一个只读常量时(read-only)IL会为你引用一个运行时常量的变量,而不是直接使用该值。
当你任意的使用其中一个常量时,这些区别就在一些限制上表现出来。编译时常量只能是基本类型(primitive types)(built-in integral and floating-poing types),枚举或者是字符串。这些就是你只能给运行时常量在初始化时赋值的类型。这些基本类就是可以被编译器在编译IL代码时直接用真实的值所取代的数据类型。下面的代码块(construct)不能通过编译。你不能用new运算符初始化一个编译时常量,即使这个数据类型是值类型。

//Does not complie, use readonly instead:
private const DateTime _classCreation = new DateTime(2000,1,1,0,0,0);

(译注:DateTime是一个值类型数据,但上面的代码因为用了new运算符,编译器无法在编译确定具体的对象应该用什么样的实际值来取代,所以无法通过编译。)

编译时常量仅限于数字和字符串。只读变量,也就是运行时常量,在构造函数(constructor)执行完成后它们是不以能被修改的。但只读变量是所有不同的,因为他们是在运行时才赋值的。当你使用运行时常量时,你有更大的可伸缩性。有一点要注意的是,运行时常量可以是任何类型的数据。而且你必须在构造函数里对他们初始化,或者你可以用任何一个初始化函数来完成。你可以添加一个DateTime结构的只读变量(--运行时常量),但你不能添加一个DateTime结构的(编译时)常量。

你可以把每一个实例(的常量)指定为只读的,从而为每一个类的实例存放不同的值。与编译时常量不同的是,它只能是静态的。
(译注:简单的讲,运行时常量可以是一个类的实例成员,也可以是一个类型的静态成员,而编译时常量只能是静态成员,因此类似:static const string m_name;的代码是不能通过编译的。)

只读数据最重要的区别是他们在运行时才确定值。当你使用只读变量 时,IL会为你产生一个对只读变量引用,而不是直接产生数值。随着时间的推移,这个区别在(系统)维护上有深远的潜在影响。
编译时常量生成的IL代码就跟直接使用数值时生成的IL是一样的,即使是在跨程序集时:一个程序集里的编译时常量在另一个程序集会保留着同样的值(译注:这里说的不是很清楚,看后面的这个例子可能会更清楚一些)。
编译时常量和运行时常量的赋值方法对运行时的兼容性有所影响。
假设你已经在程序集Infrastructure中同时定义了一个const和一个readonly变量:

public class UserfulValues{
 
public static readonly int StartValue = 5;
 
public const int EndValue = 10;
}

同时,在另一个程序集(译注:这个程序集认为是我们做测试的应用程序的程序集,下面所说的应用程序的程序集都是指的这个程序集)中,你引用了这些值:

for(int i=UserfulValues.StartValue;i<UserfulValues.EndValue;i++){
 Console.WriteLine(
"value is {0}",i);
}

如果你运行这个简单测试程序,你可以看到下面明显的结果:
value is 5
value is 6
...
value is 9
过后,你又为程序集Infrastructure发布了个新的版本,并做了如下的修改:

public class UserfulValues{
 
public static readonly int StartValue = 105;
 
public const int EndValue = 120;
}

你单独的发布了程序集Infrastructure而没有全部编译你的程序,你希望得到下面的:
value is 105
value is 106
...
value is 119
事实上,你什么也得不到。上面的循环已经是用105开始而用10来结束。C#编译器(在编译时)把常量用10来代替应用程序的程序集中的使用,而不是用常量EndValue所存储的值。而常量StartValue的值,它是被申明为只读的,它可以在运行时重新读取该常量的值。因此,应用程序的程序集可以在不用重新编译的情况下使用新的数据,简单的编译一下Infrastructure程序集,然后重新布署安装一下,就足够让你的客户可能使用这些新的数据了。更新的编译时常量应该看成是接口的变化。你必须重新编译所有引用到编译时常量的代码。更新的运行时常量则可以当成是实现的改变,这于在客户端已经存在的二进制代码是兼容的。用MSIL解释一下前面的那个循环里发生了什么:
IL_0000: ldsfld int32 Chapter1.UserfulValues::StartValue
IL_0005: stloc.0
IL_0006: br.s IL_001c
IL_0008: ldstr "value is {0}"
IL_000d: ldloc.0
IL_000e: box [mscrolib]System.Int32
IL_0013: call void [mscrolib]System.Console::WriteLine(string,object)
IL_0018: ldloc.0
IL_0019: ldc.i4.1
IL_001a: add
IL_001b: stloc.0
IL_001c: ldloc.0
IL_001d: ldc.i4.s 10
IL_001f: blt.s IL_0008
从MSIL命令清单的最上面一行你可以看到,StartValue(的值)是动态载入的。
但是,在MSIL命令的最后,结束条件是把值10当成硬代码(hard-coded)使用的。

另一方面,有些时候你也须要为某些值使用编译时常量。例如:考虑一个须要识别不同版本的续列化情形。用来标识一个特殊版本号的常量应该是一个编译时常量,它们决不会发生改变。而当前版本号则应该是一个运行时常量,在不同的版本发布后会有所改变。

private const int VERSION_1_0 = 0x0100;
private const int VERSION_1_1 = 0x0101;
private const int VERSION_1_2 = 0x0102;
//major release;
private const int VERSION_2_0 = 0x0200;
//Chech for the current version:
private static readonly int CURRENT_VERSION = VERSION_2_0;

在每次存盘时,你用运行常量来保存当前版本号。

//Read fom persistent storage, check stored version against complie-time constant:
protected MyType(SerializationInfo info, StreamingContext cntxt){
 
int storedVersion = info.GetInt32("VERSION");
 
switch(storedVersion){
 
case VERSION_2_0:
  readVersion2(info,cntxt);
  
break;
 
case VERSION_1_1:
  readVersion1(info,cntxt);
  
break;
 
//etc.  
 }

}


//Write the current version:
[SecurityPermissionAttribute(SecurityAction.Demand,SerializationFormatter = true)]
void ISerializable.GetObjectData(SerializationInfo inf,StreamingContext cxt){
 
//use runtime constant for currnet version
 inf.AddValue("VERSION",CURRENT_VERSION);
 
//
 
//write remaining delements...
}


最后一个有利的原因而使我们要使用编译时常量,就是它的性能。比起运行时常量,已知的编译时常量可以更直接有效的被访问。然而,性能上的功效是甚微的,并且应该与可伸缩性的降低进行一个权衡。Be sure to profile performace differences before giveing up the flexibility.

const的值必须在编译时被确定,(它们可以是):属性参数,枚举定义,以及一小部份你认为应该定义一个值且该值不能在不同的版本发布时发生改变的常量。
无论如何,宁愿选择伸缩性更强的运行时常量。 

举报

相关文章推荐

原则2:为你的常量选择readonly而不是const

原则2:为你的常量选择readonly而不是const Preferreadonly to const 对于常量,C#里有两个不同的版本:运行时常量和编译时常量。 因为他们有不同的表现行为,所以...

Effective C++ (2) C#中的Const和Readonly

与前一篇Effective C++ (1) C++和C中的Const 对比。 Features: readonly和const都是用来标识常量的[1]。 const可用于修饰class的field...

我是如何成为一名python大咖的?

人生苦短,都说必须python,那么我分享下我是如何从小白成为Python资深开发者的吧。2014年我大学刚毕业..

Effective C#阅读笔记-2.使用readonly而不是const变量

readonly常量为运行时常量,const常量为编译时常量;编译时常量会在编译时,所有引用编译时常量的地方都会被替换为编译时常量的值,(编译时常量只能用于numbers 和strings, enum...

C#中的静态常量(const)和动态常量(static和readonly)用法和区别

本文转自:http://hi.baidu.com/mr_handy/blog/item/dcbfcd285b74e8f498250a49.html C#拥有两种不同的常量:静态常量(compile-t...

C#学习之路,学习笔记 第八章 面向对象编程:类和对象(二)8.3.1const常量 和 8.3.2readonly常量

const常量: //const常量有两个优点,一个是由于使用了有意义的名称,和数字相比,const常量更易阅读和修改; //第二个优点是由于编译器保证它的值在程序运行过程中保持固定不变,和变量相比,...

Effective C# 原则45:选择强异常来保护程序(译)

Effective C# 原则45:选择强异常来保护程序 Item 45: Prefer the Strong Exception Guarantee 当你抛出异常时,你就在应用程序中引入了一个中...

const,readonly 常量与只读

Const是常量 Const在编译时会被编译为静态成员,它确定于编译时期,属类型级,通过类型来访问。 现在通过以下几种情况来说明const常量: (1)初始化 public const s...

规则二 对常量使用ReadOnly而不是const

1. C# 有两种类型的常量,编译类型常量和运行时常量。 编译常量比运行常量会稍快,但是没有运行常量灵活。在性能要求非常严格但是数值不会改变的情况下使用编译常量。 编译常量 public con...
返回顶部
收藏助手
不良信息举报
您举报文章:深度学习:神经网络中的前向传播和反向传播算法推导
举报原因:
原因补充:

(最多只允许输入30个字)