Author: Unknown
•16:34
使用#if/#endif 块可以在同样源码上生成不同的编译(结果),大多数debug和release两个版本。但它们决不是我们喜欢用的工具。由于#if/#endif很容易被滥用,使得编写的代码难于理解且更难于调试。程序语言设计者有责任提供更好的工具,用于生成在不同运行环境下的机器代码。C#就提供了条件属性(Conditional attribute)来识别哪些方法可以根据环境设置来判断是否应该被调用。

(译注:属性在C#里有两个单词,一个是property另一个是attribute,它们有不是的意思,但译为中文时一般都是译为了属性。property是指一个对象的性质,也就是Item1里说的属性。而这里的attribute指的是.net为特殊的类,方法或者property附加的属性。可以在MSDN里查找attribute取得更多的帮助,总之要注意:attribute与property的意思是完全不一样的。)

这个方法比条件编译#if/#endif更加清晰明白。编译器可以识别Conditional属性,所以当条件属性被应用时,编译器可以很出色的完成工作。条件属性是在方法上使用的,所以这就使用你必须把不同条件下使用的代码要写到不同的方法里去。当你要为不同的条件生成不同的代码时,请使用条件属性而不是#if/#endif块。

很多编程老手都在他们的项目里用条件编译来检测先决条件(per-conditions)和后续条件(post-conditions)。

(译注:per-conditions,先决条件,是指必须满足的条件,才能完成某项工作,而post-conditions,后续条件,是指完成某项工作后一定会达到的条件。例如某个函数,把某个对象进行转化,它要求该对象不能为空,转化后,该对象一定为整形,那么:per-conditions就是该对象不能为空,而post-conditions就是该对象为整形。例子不好,但可以理解这两个概念。)

你可能会写一个私有方法来检测所有的类及持久对象。这个方法可能会是一个条件编译块,这样可以使它只在debug时有效。

private void CheckState( )
{
// The Old way:
#if DEBUG
Trace.WriteLine( "Entering CheckState for Person" );

// Grab the name of the calling routine:
string methodName =
new StackTrace( ).GetFrame( 1 ).GetMethod( ).Name;

Debug.Assert( _lastName != null,
methodName,
"Last Name cannot be null" );

Debug.Assert( _lastName.Length > 0,
methodName,
"Last Name cannot be blank" );

Debug.Assert( _firstName != null,
methodName,
"First Name cannot be null" );

Debug.Assert( _firstName.Length > 0,
methodName,
"First Name cannot be blank" );

Trace.WriteLine( "Exiting CheckState for Person" );
#endif
}

使用#if和#endif编译选项(pragmas),你已经为你的发布版(release)编译出了一个空方法。这个CheckState()方法会在所有的版本(debug和release)中调用。而在release中它什么也不做,但它要被调用。因此你还是得为例行公事的调用它而付出小部份代价。

不管怎样,上面的实践是可以正确工作的,但会导致一个只会出现在release中的细小BUG。下面的就是一个常见的错误,它会告诉你用条件编译时会发生什么:
public void Func( )
{
string msg = null;

#if DEBUG
msg = GetDiagnostics( );
#endif
Console.WriteLine( msg );
}

这一切在Debug模式下工作的很正常,但在release下却输出的为空行。release模式很乐意给你输出一个空行,然而这并不是你所期望的。傻眼了吧,但编译器帮不了你什么。你的条件编译块里的基础代码确实是这样逻辑。一些零散的#if/#endif块使你的代码在不同的编译条件下很难得诊断(diagnose)。

C#有更好的选择:这就是条件属性。用条件属性,你可以在指定的编译环境下废弃一个类的部份函数, 而这个环境可是某个变量是否被定义,或者是某个变量具有明确的值。这一功能最常见的用法就是使你的代码具有调试时可用的声明。.Net框架库已经为你提供了了基本泛型功能。这个例子告诉你如何使用.net框架库里的兼容性的调试功能,也告诉你条件属性是如何工作的以及你在何时应该添加它:
当你建立了一个Person的对象时,你添加了一个方法来验证对象的不变数据(invariants):

private void CheckState( )
{
// Grab the name of the calling routine:
string methodName =
new StackTrace( ).GetFrame( 1 ).GetMethod( ).Name;

Trace.WriteLine( "Entering CheckState for Person:" );
Trace.Write( "\tcalled by " );
Trace.WriteLine( methodName );

Debug.Assert( _lastName != null,
methodName,
"Last Name cannot be null" );

Debug.Assert( _lastName.Length > 0,
methodName,
"Last Name cannot be blank" );

Debug.Assert( _firstName != null,
methodName,
"First Name cannot be null" );

Debug.Assert( _firstName.Length > 0,
methodName,
"First Name cannot be blank" );

Trace.WriteLine( "Exiting CheckState for Person" );
}

这这个方法上,你可能不必用到太多的库函数,让我简化一下。这个StackTrace 类通过反射取得了调用方法的的名字。这样的代价是昂贵的,但它确实很好的简化了工作,例如生成程序流程的信息。这里,断定了CheckState所调用的方法的名字。被判定(determining)的方法是System.Diagnostics.Debug类的一部份,或者是System.Diagnostics.Trace类的一部份。Degbug.Assert方法用来测试条件是否满足,并在条件为false时会终止应用程序。剩下的参数定义了在断言失败后要打印的消息。Trace.WriteLine输出诊断消息到调试控制台。因此,这个方法会在Person对象不合法时输出消息到调试控制台,并终止应用程序。你可以把它做为一个先决条件或者后继条件,在所有的公共方法或者属性上调用这个方法。
public string LastName
{
get
{
CheckState( );
return _lastName;
}
set
{
CheckState( );
_lastName = value;
CheckState( );
}
}

在某人试图给LastName赋空值或者null时,CheckState会在第一时间引发一个断言。然后你就可以修正你的属性设置器,来为LastName的参数做验证。这就是你想要的。

但这样的额外检测存在于每次的例行任务里。你希望只在调试版中才做额外的验证。这时候条件属性就应运而生了:
[ Conditional( "DEBUG" ) ]
private void CheckState( )
{
// same code as above
}

Conditional属性会告诉C#编译器,这个方法只在编译环境变量DEBUG有定义时才被调用。同时,Conditional属性不会影响CheckState()函数生成的代码,只是修改对函数的调用。如果DEBGU标记被定义,你可以得到这:
public string LastName
{
get
{
CheckState( );
return _lastName;
}
set
{
CheckState( );
_lastName = value;
CheckState( );
}
}
如果不是,你得到的就是这:
public string LastName
{
get
{
return _lastName;
}
set
{
_lastName = value;
}
}

不管环境变量的状态如何,CheckState()的函数体是一样的。这只是一个例子,它告诉你为什么要弄明白.Net里编译和JIT之间的区别。不管DEBUG环境变量是否被定义,CheckState()方法总会被编译且存在于程序集中。这或许看上去是低效的,但这只是占用一点硬盘空间,CheckState()函数不会被载入到内存,更不会被JITed(译注:这里的JITed是指真正的编译为机器代码),除非它被调用。它存在于程序集文件里并不是本质问题。这样的策略是增强(程序的)可伸缩性的,并且这样只是一点微不足道的性能开销。你可以通过查看.Net框架库中Debug类而得到更深入的理解。在任何一台安装了.Net框架库的机器上,System.dll程序集包含了Debug类的所有方法的代码。由环境变量在编译时来决定是否让由调用者来调用它们。

你同样可以写一个方法,让它依懒于不只一个环境变量。当你应用多个环境变量来控制条件属性时,他们时以or的形式并列的。例如,下面这个版本的CheckState会在DEBUG或者TRACE为真时被调用:
[ Conditional( "DEBUG" ),
Conditional( "TRACE" ) ]
private void CheckState( )

如果要产生一个and的并列条件属性,你就要自己事先直接在代码里使用预处理命令定义一个标记:
#if ( VAR1 && VAR2 )
#define BOTH
#endif

是的,为了创建一个依懒于前面多个环境变量的条件例程(conditional routine),你不得不退到开始时使用的#if实践中了。#if为我们产生一个新的标记,但避免在编译选项内添加任何可运行的代码。

Conditional属性只能用在方法的实体上,另外,必须是一个返回类型为void的方法。你不能在方法内的某个代码块上使用Conditional,也不能在一个有返回值的方法上使用Conditional属性。取而代之的是,你要细心构建一个条件方法,并在那些方法上废弃条件属性行为。你仍然要回顾一下那些具有条件属性的方法,看它是否对对象的状态具有副作用。但Conditional属性在安置这些问题上比#if/#endif要好得多。在使用#if/#endif块时,你很可能错误的移除了一个重要的方法调用或者一些配置。

前面的例子合用预先定义的DEBUG或者TRACE标记,但你可以用这个技巧,扩展到任何你想要的符号上。Conditional属性可以由定义标记来灵活的控制。你可以在编译命令行上定义,也可以在系统环境变量里定义,或者从源代码的编译选择里定义。

使用Conditional属性可以比使用#if/#endif生成更高效的IL代码。在专门针对函数时,它更有优势,它会强制你在条件代码上使用更好的结构。编译器使用Conditional属性来帮助你避免因使用#if/#endif而产生的常见的错误。条件属性比起预处理,它为你区分条件代码提供了更好的支持。

=====================================
小结:翻译了几篇了,感觉书写的有点冗余,有些问题可以很简单的说明的。可能是我的理解不到位,总之,感觉就是一个问题说来说去。另外,这里例举的几个例子感觉也不是很好,特别是前一个Item里的强制转化,感觉很牵强。不管怎样,还是认真的把书读好,译好吧。
还是那句话,或者我翻译的不好,或者网上已经有更好的翻译了,或者中文版也出来了,但我还是会坚持翻译下去。但以后的翻译不会再放在博客园的首页了。担心自己的翻译不好,以后的翻译都带上原文。总之,希望对读者有帮助!

分开了一个子类,专门POST Effective C#的翻译。
Author: Unknown
•16:33
C#是一个强数据类型语言。好的编程实践意味着当可以避免从一种数据类型强制转化为另种数据类型时,我们应该尽我们的所能来避免它。但在某些时候,运行时类型检测是不可避免的。在C#里,大多数时候你要为调用函数的参数使用System.Object类型,因为Framwork已经为我们定义了函数的原型。你很可能要试图把那些类型进行向下转化为其它类型的接口或者类。你有两个选择:用as运算符,或者,采用旧式的C风格,强制转换。(不管是哪一种,)你还必须对变量进行保护:你可以试着用is进行转换,然而再用as进行转换或者强制转换。

无论何时,正确的选择是用as运算符进行类型转换。因为比起盲目的强制转换它更安全,而且在运行时效率更高。用as和is运算符进行转换时,并不是对所有的用户定义的类型都能完成的。它们只在运行时类型和目标类型匹配的时候,转换才能成功。它们决不会构造一个新的对象来满足(转化)要求。
看一个例子。你写了一段代码,要转换一个任意类型的对象实例到一个MyType类型的实例。你是这样写代码的:
object o = Factory.GetObject( );
// Version one:
MyType t = o as MyType;
if ( t != null )
{
// work with t, it's a MyType.
} else
{
// report the failure.
}
或者你这样写:
object o = Factory.GetObject( );
// Version two:
try {
MyType t;
t = ( MyType ) o;
if ( t != null )
{
// work with T, it's a MyType.
} else
{
// Report a null reference failure.
}
} catch
{
// report the conversion failure.
}
你会同意第一种写法更简单更容易读。它没有try/catch结构,所以你可以同时避免(性能)开销和(多写)代码。我们注意到,强制转换的方法为了检测转换是否把一个null的对象进行强制转换,而不得不添加一个捕获异常的结构。null可以被转换为任意的引用类型,但as操作符就算是转化一个null的引用时,也会(安全的)返回一个null。所以当你用强制类型转换时,就得用一个try/catch结构来捕获转换null时的异常。用as进行转换的时就,就只用简单的检测一下转化后的实例不为null就行了。
(译注:被转换对象和转换后的结果都有可能为null,上面就是对这两种null进行了说明,注意区分。强制转换是不安全的,可能会有异常抛出,因此要用try/catch结构来保证程序正常运行,而as转换是安全的,不会有异常抛出,但在转换失败后,其结果为null)

强制转换与as转换最大的区别表现在如何对待用户定义类型的转换。
与其它运算不一样,as和is运算符在运行时要检测转换目标的类型。如果一个指定对象不是要求转换的类型,或者它是从要求转换类型那里派生的,转换会失败。另一方面,强制转换可以用转换操作把一个对象转换成要求的类型。这还包括对内置数据(built-in numberic)类型的转换。强制转换一个long到一个short可能会丢失数据。
同样的问题也隐藏在对用户定义类型的转换上。考虑这样的类型:
public class SecondType
{
private MyType _value;
// other details elided
// Conversion operator.
// This converts a SecondType to
// a MyType, see item 29.
public static implicit operator MyType( SecondType t )
{
return t._value;
}
}

假设代码片段中开始的Factory.GetObject()函数返回的是SecondType 类型的数据:
object o = Factory.GetObject( );
// o is a SecondType:
MyType t = o as MyType; // Fails. o is not MyType
if ( t != null )
{
// work with t, it's a MyType.
} else
{
// report the failure.
}
// Version two:
try {
MyType t1;
t = ( MyType ) o; // Fails. o is not MyType
if ( t1 != null )
{
// work with t1, it's a MyType.
} else
{
// Report a null reference failure.
}
} catch
{
// report the conversion failure.
}
两种转换都失败了。但是我告诉过你,强制转化可以在用户定义的类型上完成。你应该想到强制转化会成功。你是对的--(如果)它们跟像你想的一样是会成功的。但是转换失败了,因为你的编译器为对象o产生的代码是基于编译时类型。而对于运行时对象o,编译器什么也不知道,它们被视为System.Obejct类型。编译器认为,不存在System.Object类型到用户类型MyType的转换。它检测了System.Object和MyType的定义。缺少任意的用户定义类型转换,编译器(为我们)生成了用于检测运行时对象o的代码,并且检测它是不是MyType类型。因为对象o是SecondType类型,所以失败了。编译器并不去检测实际运行时对象o是否可以被转换为MyType类型。

如果你使用下面的代码段,你应该可以成功的完成从SecondType到MyType的转换:

object o = Factory.GetObject( );

// Version three:
SecondType st = o as SecondType;
try {
MyType t;
t = ( MyType ) st;
if ( t != null )
{
// work with T, it's a MyType.
} else
{
// Report a null reference failure.
}
} catch
{
// report the failure.
}

你决不应该写出如果糟糕的代码,但它确实解决了一个很常见的难题。尽管你决不应该这样写代码,但你可以写一个函数,用一个System.Object参数来完成正确的转换:
object o = Factory.GetObject( );
DoStuffWithObject( o );
private void DoStuffWithObject( object o2 )
{
try {
MyType t;
t = ( MyType ) o2; // Fails. o is not MyType
if ( t != null )
{
// work with T, it's a MyType.
} else
{
// Report a null reference failure.
}
} catch
{
// report the conversion failure.
}
}

记住,对一个用户定义类型的对象,转换操作只是在编译时,而不是在运行时。在运行时存在介于o2和MyType之间的转换并没有关系,(因为)编译器并不知道也不关心这些。这样的语句有不同的行为,这要取决于对st类型的申明:
t = ( MyType ) st;
(译注:上面说的有些模糊。为什么上面的代码可能会有不同的行为的呢?不同的什么行为呢?主要就是:上面的这个转化,是在编译时还是在运行时!如果st是用户定义的类型,那么上面的转换是在编译时。编译器把st当成为System.Object类型来编译生成的IL代码,因此在运行时是无法把一个Object类型转化为MyType类型的。解决办法就是前面提到的方法,多加一条语句,先把Object类型转换为SecondType,然后再强制转化为MyType类型。但是如果st是内置类型,那么它的转换是在运行时的,这样的转化或许会成功,看后面的说明。因此,类似这样的代码:MyType m_mytype = (m_secondType as SecondType) as MyType;是不能通过编译的,提示错误是无法在编译时把SecondType转化为MyType,即使是重写了转换操作符。)

但下面的转换只会有一种行为,而不管st是什么类型。
所以你应该选择as来转换对象,而不是强制类型转换。实际上,如果这些类型与继承没有关系,但是用户自己定义的转换操作符是存在的,那么下面的语句转换将会得到一个编译错误:t = st as MyType;现在你应该明白要尽可能的使用as,下面我们来讨论不能使用as的时候。as运算符对值类型是无效的,下面的代码无法通过编译:
object o = Factory.GetValue( );
int i = o as int; // Does not compile.
这是因为整形(ints)数据是值类型,并且它们永远不会为null。当o不是一个整形的时候,i应该取什么值呢?不管你选择什么值,它都将是一个无效的整数。因此,as不能使用(在值类型数据上)。你可以坚持用强制转化:
object o = Factory.GetValue( );
int i = 0;
try {
i = ( int ) o;
} catch
{
i = 0;
}

但是你并没有必要这样坚持用强制转换。你可以用is语句取消可能因转换引发的异常:
object o = Factory.GetValue( );
int i = 0;
if ( o is int )
i = ( int ) o;
(译注:is和as一样,都是类型转换安全的,它们在任何时候都不会在转换时发生异常,因此可以先用is来安全的判断一下数据类型。与as不同的时,is只是做类型检测并返回逻辑值,不做转换。)

如果o是其它可转化为整形的类型(译注:但o并不是真正的整形),例如double,那么is运算操作会返回false。对于null,is总是返回false。

is只应该在你无法用as进行转换时使用。
另外,这是无意义的冗余:
// correct, but redundant:
object o = Factory.GetObject( );

MyType t = null;
if ( o is MyType )
t = o as MyType;

如果你写下面的代码,那么跟上面一样,都是冗余的:
// correct, but redundant:
object o = Factory.GetObject( );

MyType t = null;
if ( ( o as MyType ) != null )
t = o as MyType;

这都是低效且冗余的。如果你使用as来转换数据,那么用is来做检测是不必要的。只用检测返回类型是否为null就行了,这很简单。

现在,你已经知道is,as和强制转换之间的区别了。而在foreach的循环中,是使用的哪一种转换呢?
public void UseCollection( IEnumerable theCollection )
{
foreach ( MyType t in theCollection )
t.DoStuff( );
}

foreach循环是用强制转换来完成把一个对象转换成循环可用的类型。上面的循环代码与下面手写的代码(hand-coded)是等效的:
public void UseCollection( IEnumerable theCollection )
{
IEnumerator it = theCollection.GetEnumerator( );
while ( it.MoveNext( ) )
{
MyType t = ( MyType ) it.Current;
t.DoStuff( );
}
}

foreach须要用强制转换来同时支持对值类型和引用类型的转换。通过选择强制转化,foreach循环就可以采用一样的行为,而不用管(循环)目标对象是什么类型。不管怎样,因为foreach循环是用的强制转换,因些它可能会产生BadCastExceptions的异常。

因为IEnumberator.Current返回一个System.Obejct对象,该对象没有(重写)转换操作符,所以它们没有一个满足(我们在上面做的)测试。
(译注:这里是说,如果你用一个集合存放了SecondType,而你又想用MyType来对它进行foreach循环,那么转换是失败的,原因是在循环时,并不是用SecondType,而是用的System.Object,因此,在foreach循环里做的转换与前面说的:MyType t = ( MyType ) o;是一样的错误,这里的o是SecondType,但是是以System.Object存在。)

正如你已经知道的,一个存放了SecondType的集合是不能在前面的函数UseCollection中使用循环的,这会是失败的。用强制转换的foreach循环不会在转换时检测循环集合中的对象是否具有有效的运行时类型。它只检测由IEnumerator.Current返回来的System.Object是否可转换为循环中使用的对象类型,这个例子中是MyType类型。

最后,有些时候你想知道一个对象的精确类型,而不仅仅是满足当前可转换的目标类型。as运算符在为任何一个从目标类型上派生的对象进行转换时返回true。GetType()方法取得一个对象的运行时对象,它提供了比is和as更严格的(类型)测试。GetType()返回的类型可以与指定的类型进行比较(,从而知道它是不是我们想要的类型)。

再次考虑这个函数:
public void UseCollection( IEnumerable theCollection )
{
foreach ( MyType t in theCollection )
t.DoStuff( );
}
如果你添加了一个派生自MyType的新类NewType,那么一个存放了NewType类型对象的集合可以很好的在UseCollection 函数中工作。
public class NewType : MyType
{
// contents elided.
}

如果你想要写一个函数,使它对所有MyType类型的实例都能工作,上面的方法是非常不错的。如果你只想写一个函数,只对精确的MyType对象有效,那你必须用精确的类型比较。这里,你可以在foreach循环的内部完成。大多数时候,认为运行时确定对象的精确类型是很重要的,是在为对象做相等测试的时候。在大多数其它的比较中,由is和as提供的.isinst(译注:IL指令)比较,从语义上讲已经是正确的了。

好的面向对象实践告诉我们,你应该避免类型转换,但有些时候我没别无选择。当你无法避免转换时,用(C#)语言为我们提供的is和as运算符来清楚的表达你的意思吧。不同方法的强制转换有不同的规则。从语义上讲,is和as在绝大多转换上正确的,并当目标对象是正确的类型时, 它们总是成功的。应该选择这些基本指令来转换对象,至少它们会如你所期望的那样成功或者失败;而不是选择强制类型转换,这转换会产生一些意想不到的副作用。

=======================================================
小结:翻译的第三个原则。
今天下午在网上下载了电子版的书籍,因此翻译相对快一点点,至少不用自己输入代码了,那是很郁闷的事。继续翻译,争取在春节休假期间把所有的Items翻译完。
我对书中所说的要注意的事项基本上都自己测试一下,当然,如果是显然正确的问题,就直接翻译了,不做测试。对于一些有疑点的地方,我尽可能的自己动手测试一下。继续努力,相信后面的翻译会越来越快。
Author: Unknown
•16:30
对于常量,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 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的值必须在编译时被确定,(它们可以是):属性参数,枚举定义,以及一小部份你认为应该定义一个值且该值不能在不同的版本发布时发生改变的常量。
无论如何,宁愿选择伸缩性更强的运行时常量。

============================
小结:第一次翻译。昨天花了半天时间,今天又花了几个小时。终于完成了Item2的翻译。感觉很吃力。或者我的翻译很糟糕,或者已经有更好的翻译了,但不管怎样,这也算是我自己辛苦劳动的结果。希望对读者有所帮助。本想在翻译中添加一我自己的看法,那样的话对原文的改动可能比较大,所以没做太多注解。我会继续努力翻译完后面的Items,不管翻译的怎样,也不管是不是有其它的翻译已经完成了。这对自己也是一个学习过程。我会在后面的翻译中一些我自己的看法,当然尽可能的保留原文的样子,实在不好办的,我就会以读书笔记的形式另外写文章。
另外,我也不知道这样的文章能不能发在首页,如果不行,以后的就都发到其它区吧。