改善C#程序的50种_第1页
改善C#程序的50种_第2页
改善C#程序的50种_第3页
改善C#程序的50种_第4页
改善C#程序的50种_第5页
已阅读5页,还剩69页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

为什么程序已经可以正常工作了,我们还要改变它们呢?答案就是我们可以让它们变得更好。我们常常会改变所使

用的工具或者语言,因为新的工具或者语言更富生产力。如果固守旧有的习惯,我们将得不到期望的结果。对于C#

这种和我们已经熟悉的语言(如C++或Java)有诸多共通之处的新语言,情况更是如此。人们很容易回到I日的习惯

中去。当然,这些旧的习惯绝大多数都很好,C#语言的设计者们也确实希望我们能够利用这些旧习惯下所获取的知

识。但是,为了让C#和公共语言运行库(CommonLanguageRuntime,CLR)能够更好地集成在一起,从而为面向组

件的软件开发提供更好的支持,这些设计者们不可避免地需要添加或者改变某些元素。本章将讨论那些在C#中应该

改变的旧习惯,以及对应的新的推荐做法。

条款1:使用属性代替可访问的数据成员

C#将属性从其他语言中的一种特殊约定提升成为一种第一等(first-class)的语言特性。如果大家还在类型中定义公

有的数据成员,或者还在手工添加get和set方法,请赶快停下来。属性在使我们可以将数据成员暴露为公有接口

的同时,还为我们提供了在面向对象环境中所期望的封装。在C#中,属性(property)是这样一种语言元素:它们

在被访问的时候看起来好像是数据成员,但是它们却是用方法实现的。

有时候,一些类型成员最好的表示形式就是数据,例如一个客户的名字、一个点的x/y坐标,或者上一年的收入。

使用属性我们可以创建一种特殊的接口一一这种接口在行为上像数据访问,但却仍能获得函数的全部好处。客户代

码乜1对属性的访问就像访问公有变量一样。但实际的实现采用的却是方法,这些方法内部定义了属性访问器的行为。

.NET框架假定我们会使用属性来表达公有数据成员。事实上,.NET框架中的数据绑定类只支持属性,而不支持公有

数据成员。这些数据绑定类会将对象的属性关联到用户界面控件(Web控件或者WindowsForms控件)上。其数据

绑定机制事实上是使用反射来查找一个类型中具有特定名称的属性。例如下面的代码:

textBoxCity.DataBindings.Addf'Text",

address,"City");

便是将textBoxCity控件的Text属性和address对象的City属性绑定在一起。(有关数据绑定的细节,参见条款38。)

如果City是个公有数据成员,这样的数据绑定就不能正常工作。.NET框架类库(FrameworkClassLibrary)的设计

者们之所以不支持这样的做法,是因为将数据成员直接暴露给外界不符合面向对象的设计原则。.NET框架类库这样

的设计策略从某种意义上讲也是在推动我们遵循面向对象的设计原则。对于C++和Java编程老手,我想特别指出的

是这些数据绑定代码并不会去查找get和set函数。在C#中,我们应该忘掉get_和set_这些旧式的约定,而全面采

用属性。

当然,数据绑定所应用的类一般都要和用户界面打交道。但这并不意味着属性只在UI(用户界面)逻辑中有用武之

地。对于其他类和结构,我们也需要使用属性。随着时间的推移,新的需求或行为往往会影响原来类型的实现,采

用属性比较容易能够应对这些变化。例如,我们可能很快就会发现Customer类型不能有一个空的Name。如果我们

使用•个公用属性来实现Name,那么只需要在一个地方做更改即可:

publicclassCustomer

(

privatestring_name;

publicstringName

(

get

(

return_name;

)

set

(

if{(value==null)11

(value.Length==0))

thrownewArgumentException("Namecannotbeblank",

"Name");

_name=value;

)

)

//…

如果使用的是公有数据成员,我们就要寻找并修改所有设置Customer的Name的代码,那将花费大量的时间。

另外,由于属性是采用方法来实现的,因此为它们添加多线程支持就更加容易一一直接在get和set方法中提供同步

数据访问控制即可:

publicstringName

(

get

{

lock(this)

(

return_name;

}

}

set

(

lock(this)

(

_name=value;

)

)

)

既然是采用方法来实现的,那么属性也就具有了方法所具有的全部功能。比如,属性可以实现为虚属性:

publicclassCustomer

(

privatestring_name;

publicvirtualstringName

(

get

(

return__name;

)

set

|

_name=value;

)

)

〃忽略其他实现代码。

)

自然,属性也可以实现为抽象属性,或者作为接口定义的一部分:

publicinterfaceINameValuePair

(

objectName

(

get;

}

objectValue

(

get;

set;

最后,我们还可以借助属性的特点来创建const和非const版本的接口:

publicinterfaceIConstNameValuePair

(

objectName

(

get;

)

objectValue

(

get;

}

)

publicinterfaceINameValuePair

(

objectValue

(

get;

set;

)

)

//上述接口的应用:

publicclassStuff:IConstNameValuePair,INameValuePair

(

privatestring_name;

privateobject_value;

#regionIConstNameValuePairMembers

publicobjectName

{

get

(

return_name;

)

}

objectIConstNameValuePair.Value

|

get

(

return_value;

}

)

#endregion

#regionINameValuePairMembers

publicobjectValue

{

get

return_value;

set

_value=value;

)

)

#endregion

}

属性在C#中已经成为一项比较完善的、第一等的语言元素。我们可以针对成员函数做的任何事情,对于属性也同样

适用。毕竟,属性是对访问/修改内部数据的方法的一种扩展。

我们知道,属性访问器在编译后事实上是两个分离的方法。在C#2.0中,我们可以为一个属性的get访问器和set

访问器指定不同的访问修饰符。这使得我们可以更好地控制属性的可见性。

//合法的C#2.0代码:

publicclassCustomer

(

privatestring_name;

publicvirtualstringName

(

get

(

return_name;

}

protectedset

(

_name=value;

)

)

〃忽略其他实现代码。

}

C#的属性语法扩展自简单的数据字段。如果类型接口需要包含一些索引数据项,则可以使用一种称作索引器

(indexer)的类型成员。索引器在C#中又称含参属性(parameterizedproperty)»这种“使用属性来返回•,个序列

中的数据项"的做法对于很多场合非常有用,下面的代码展示了这一用法:

publicintthis[intindex]

|

get

{

return_theValues[index];

)

set

(

_theValues[index]=value;

)

)

〃访问索引器:

intval=MyObject[i];

索引器和一般的属性(即支持单个数据项的属性)在C#中有同样的语言支持,它们都用方法实现,我们可以在其内

部做任何校验或者计算工作。索引器也可以为虚索引器,或者抽象索引器。它们可以声明在接口中,也可以成为只

读索引器或者读一写索引器。以数值作为参数的"一维索引器”还可以参与数据绑定。使用非数值的索引器则可以用

来定义m叩或者dictionary等数据结构:

publicAddressthis[stringname]

(

get

{

return_theValues[name];

)

set

{

_theValues[name]=value;

)

)

与C#中的多维数组类似,我们也可以创建“多维索引器”一一其每一维上的参数类型可以相同,也可以不同。

publicintthis[intx,inty]

(

get

(

returnComputeValue(x,y);

publicintthis[intx,stringname]

(

get

(

returnComputeValue(x,name);

)

)

注意所有的索引器都使用this关键字来声明。我们不能为索引器指定其他的名称。因此,在每个类型中,对于同样

的参数列表,我们只能有一个索引器。

属性显然是一个好东西,相较于以前的各种访问方式来讲,它的确是一个进步。但是,有些读者可能会有如下的想

法:刚开始先使用数据成员,之后如果需要获得属性的好处时,再考虑将数据成员替换为属性。这种做法听起来似

乎有道理,但实际上是错的。让我们来看下面一段代码:

//使用公有数据成员,不推荐这种做法:

publicclassCustomer

|

publicstringName;

//忽略其他实现代码。

)

这段代码描述了一个Customer类,其内包含一个成员Name。我们可以使用成员访问符来获取/设置其Name的值:

stringname=customerOne.Name;

customerOne.Name="ThisCompany,Inc.";

这段代码非常简洁和直观。有人据此就认为以后如果有需要,再将Customer类的数据成员Name替换为属性就可

以了,而使用Customer类型的代码无需做任何改变。这种说法从某种程度上来讲是对的。

属性在被访问的时候和数据成员看起来没有什么差别。这正是C#引入新的属性语法的一个目标。但属性毕竟不是数

据,访问属性和访问数据产生的是不同的MSIL。前面那个Customer类型的Name字段在编译后将产生如下MSIL代

码:

.fieldpublicstringName

而访问该字段的部分编译后的MSIL代码如下:

ldloc.0

IdfldstringNameSpace.Customer::Name

stloc.l

向该字段存储数据的部分编译后的MSIL代码如下:

ldloc.0

Idstr"ThisCompany,Inc."

stfldstringNameSpace.Customer::Name

大家不必担忧,我们不会整天围绕着IL代码转。为了让大家清楚"在数据成员和属性之间做改变会打破二进制兼容

性",在这里展示一下IL代码还是很重要的。我们再来看下面的Customer类型实现,这次我们采用了属性的方案:

publicclassCustomer

(

privatestring_name;

publicstringName

{

get

(

return_name;

}

set

{

_name=value;

)

)

〃忽略其他实现代码。

)

当我们在C#中访问Name属性时,使用的语法和前面访问字段的语法•模•样。

stringname=customerOne.Name;

customerOne.Name="ThisCompany,Inc.";

但是,C#编译器对于两段相同的C#代码产生的却是完全不同的MSIL代码。我们来看新版Customer类型的Name属

性编译后的MSIL:

.propertyinstancestringName()

(

.getinstancestringNameSpace.Customer::get_Name()

.setinstancevoidNameSpace.Customer::set_Name(string)

}//属性Customer::Name结束。

.methodpublichidebysigspecialnameinstancestring

get_Name()cilmanaged

(

〃代码长度11(Oxb)

.maxstack1

.localsinit([0]stringCS$00000003$00000000)

IL_0000:ldarg.0

IL_0001:IdfldstringNameSpace.Customer::_name

IL_0006:stloc.O

IL_0007:br.sIL_0009

IL_0009:ldloc.0

IL_000a:ret

}//方法Customer::get_Name结束。

.methodpublichidebysigspecialnameinstancevoid

set_Name(string'value')cilmanaged

//代码长度8(0x8)

.maxstack2

IL_0000:ldarg.0

IL_0001:Idarg.l

IL0002:stfldstringNameSpace.Customer::_name

IL_0007:ret

}//方法Customer::set_Name结束。

在将属性定义从C#代码转换为MSIL的过程中,有两点需要我们注意:首先,.property指示符定义了属性的类型,

以及实现属性get访问器和set访问器的两个函数。这两个函数被标记为hidebysig和specialname。对我们来说,这

两个标记意味着它们所修饰的函数不能直接在C#源代码中被调用,也不被认为是类型正式定义的一部分。要访问它

们,我们只能通过属性。

当然,大家对于属性定义产生不同的MSIL应该早有预期。更重要的是,对属性所做的get和set访问的客户代码编

译出来的MSIL也不同:

//get

ldloc.0

callvirtinstancestringNameSpace.Customer::get_Name()

stloc.l

//set

ldloc.0

Idstr"ThisCompany,Inc."

callvirtinstancevoidNameSpace.Customer::set_Name(string)

大家看到了,同样是访问客户(Customer)名称(Name)的C#源代码,由于所使用的Name成员不同--属性或

者数据成员,编译后产生出的MSIL指令也不同。尽管访问属性和访问数据成员使用的是同样的C#源代码,但是C#

编译器却将它们转换为不同的IL代码。

换句话说,虽然属性和数据成员在源代码层次上是兼容的,但是在二进制层次上却不兼容。这意味着如果将一个类

型的公有数据成员改为公有属性,那么我们必须重新编译所有使用该公有数据成员的C#代码。本书第4章"创建二

进制组件”讨论了二进制组件的相关细节,但是在此之前大家要清楚,将一个数据成员改为属性会破坏二进制兼容

性。如果这样的程序集已经被部署,那么升级它们的工作将变得非常麻烦。

看了属性产生的1L代码之后,有读者可能想知道使用属性和使用数据成员在性能上有什么差别。虽然使用属性不会

比使用数据成员的代码效率更快,但是它也不见得就会比使用数据成员的代码慢,因为Jn■编译器会对某些方法调

用(包括属性访问器)进行内联处理。如果JH■编译器对属性访问器进行了内联处理,那么属性和数据成员的效率

将没有任何差别。即使属性访问器没有被内联,实际的效率差别相对于函数调用的成本来讲也是可以忽略不计的。

只有在很少的一些情况下,这种差别才值得我们注意。

综上所述,只要打算将数据暴露在类型的公有接口或者受保护接口中,我们都应该使用属性来实现。对于具有序列

或者字典特征的类型,则应该采用索引器。所有的数据成员都应一律声明为私有。使用属性的好处显而易见:我们

可以得到更好的数据绑定支持,我们可以更容易地在将来对其访问方法的实现做任何改变。将变量封装在属性中只

不过增加一两分钟代码录入时间。如果刚开始使用数据成员,后来又发现需要使用属性,这时再来修改的成本将是

几个小时。今天的一点投入,会为明天节省许多时间。

条款2:运行时常量(readonly)优于编译时常量(const)

C#语言有两种不同的常量机制:一种为编译时(compile-time)常量,一种为运行时(runtime)常量。两种常量

有着非常迥异的行为,使用不正确会导致程序的性能下降或者出现错误。这两种代价,哪一个都没有人愿意承担,

但是如果必须承担一个,那么“慢、但是能够正确运行的”程序总比“快、但是可能出错的”程序要好。因此,我

们说运行时常量优于编译时常量。编译时常量比运行时常量稍微快一点,但却缺乏灵活性。只有在性能非常关键,

并且其值永远不会改变的情况下,我们才应该使用编译时常量。

在C#中,我们使用readonly关键字来声明运行时常量,用const关键字来声明编译时常量.

//编译时常量:

publicconstint-Millennium=2000;

//运行时常量:

publicstaticreadonlyint_ThisYear=2004;

编译时常量与运行时常量行为的不同处在于它们的访问方式。编译时常量在编译后的结果代码中会被替换为该常量

的值,例如下面的代码:

if(myDateTime.Year==Millennium)

其编译后的IL和下面的代码编译后的1L•样:

if(myDateTime.Year==2000)

条款2:运行时常量(readonly)优于编译时常量(const)16

运行时常量的值则在运行时被计算。对于使用运行时常量的代码,其编译后的IL将维持对readonly变量(而非它

的值)的引用。

这种差别会为我们使用两种常量类型带来一些限制。编译时常量只可以用于基元类型(包括内建的整数类型和浮点

类型)、枚举类型或字符串类型。因为只有这些类型才允许我们在初始化器中指定有意义的常量值回。在使用这

些常量的代码编译后得到的IL代码中,常量将直接被替换为它们的字面值(literal)。例如,下面的代码就不会

通过编译。事实上,C#不允许我们使用new操作符来初始化一个编译时常量,即使被初始化的常量类型为一个值类

型。

//下面的代码不会通过编译,但是换成readonly就可以:

privateconstDateTime_classCreation=new

DateTime(2000,1,1,0,0,0);

编译时常量仅限于数值和字符串。只读(read-only)字段之所以也被称作利常量,是因为它们的构造器一旦被

执行,我们将不能对它们的值做任何修改。与编译时常量不同的地方在于,只读字段的赋值操作发生在运行时,

因此它们具有更多的灵活性。比如,只读字段的类型就没有任何限制。对于只读字段,我们只能在构造器或者初始

化器中为它们赋值。在上面的代码中,我们可以声明readonly的DateTime结构变量,但是却不能声明const的

DateTime结构变量。

我们可以声明readonly的实例常量,从而为一个类型的每个实例存储不同的值。但是const修饰的编译时常量默

认就被定义为静态常量。

我们知道,运行时常量和编译时常量最重要的区别就在于运行时常量值的辨析发生在运行时,而编译时常量值的

辨析发生编译时。换言之,使用运行时常量编译后的IL代码引用的是readonly变量,而非它的值;而使用编译

时常量编译后的IL代码将直接引用它的值——就像我们直接在代码中使用常量值一样。即使我们使用的是数值常

量并跨程序集引用,情况也是一样:如果在程序集A中引用程序集B中的常量,那么编译后程序集A中出现的那

个常量将被它的值所替换。这种差别对于代码的维护性而言有着相当的影响。

编译时常量与运行时常量被辨析的方式影响着运行时的兼容性。假设我们在一个名为Infrastructure的程序集中

分别定义了一个const字段和一个readonly字段:

publicclassUsefulValues

{

publicstaticreadonlyintStartValue=5;

publicconstintEndValue=10;

)

在另外一个程序集Application中,我们又引用着这些值:

for(inti=UsefulValues.StartValue;

i<UsefulValues.EndValue;

i++)

Console.WriteLine("valueis{0}",i);

如果我们运行上面的代码,将得到以下输出:

Valueis5

Valueis6

Valueis9

假设随着时间的推移,我们又发布了一个新版的Infrastructure程序集:

publicclassUsefulValues

publicstaticreadonlyintStartValue=105;

publicconstintEndValue=120;

)

我们将新版的Infrastructure程序集分发出去,但并没有重新编译Application程序集。我们期望得到如下的输

出:

Valueis105

Valueis106

Valueis119

但实际上,我们却没有得到任何输出。因为现在那个循环语句将使用105作为它的起始值,使用10作为它的结束条

件。其根本原因在于C#编译器在第一次编译Application程序集时,将其中的EndValue替换成了它对应的常量值

10o而对于StartValue来说,由于它被声明为readonly,所以它的辨析发生在运行时。因此,Application程序

集在没有被重新编译的情况下,仍然可以使用新的StartValue值。为了改变所有使用readonly常量的客户代码

的行为,简单地安装一个新版的Infrastructure程序集就足够了。“更改一个运行时常量的值”应该被视作对类

型接口的更改,其后果是我们必须重新编译所有引用该常量的代码。“更改•个公有的运行时常量的值”应该被

视作对类型实现的更改,它与其客户代码在二进制层次上是兼容的。大家看看上述代码中的循环编译后的MSIL,就

会对这里所谈的更加清楚了:

IL0000:Idsfldint32Chapterl.UsefulValues::StartValue

IL._0005:stloc.0

IL._0006:br.sIL_001c

IL._0008:Idstr“valueis{0}〃

IL._000d:Idloc.0

IL._000e:box[mscorlib]System.Int32

IL._0013:callvoid[mscorlib]System.Console::WriteLine

(string,object)

IL._0018:Idloc.0

IL._0019:Ide.i4.1

IL._001a:add

IL__001b:stloc.0

IL__001c:Idloc.0

IL__001d:Ide.i4.s10

IL_001f:bit.sIL_0008

大家可以在这段MSIL代码的顶端看到StartValue的确是被动态加载的,而在其末尾可以看到结束条件被硬编码

(hard-code)为10。

不过,有时候有些值确实可以在编译时确定,这时候就应该使用编译时常量。例如,考虑在对象的序列化形式(有

关对象序列化,可参见条款25)中使用一组常量来区分不同版本的对象。其中,标记特殊版本号的持久化数据应

该采用编译时常量,因为它们的值永远不会改变。但是标记当前版本号的数据应该采用运行时常量,因为它的值会

随着每个不同的版本而改动。

privateconstintVERSION_1_O=0x0100;

privateconstintVERSION_1_1=0x0101;

privateconstintVERSION」_2=0x0102;

//主发行版本:

privateconstintVERSI0N_2_0=0x0200;

//标记当前版本:

privatestaticreadonlyintCURRENTVERSION=

VERSI0N_2_0;

我们使用运行时版本⑶来将当前的版本号存储在每•个序列化文件中:

//从持久层数据源读取对象,将存储的版本号与编译时常量相比对:

protectedMyType(Serializationinfoinfo,

StreamingContextcntxt)

(

intstoredVersion=info.Getlnt32("VERSION");

switch(storedVersion)

(

caseVERSION20:

readVersion2(info,cntxt);

break;

caseVERSION1_1:

readVersionlDotl(info,cntxt);

break;

〃忽略其他细节。

)

)

//写入当前版本号:

[SecurityPermissionAttribute(SecurityAction.Demand,

SerializationFormatter=true)]

voidISerializable.GetObjectData(Serializationinfoinf,

StreamingContextext)

(

//使用运行时常量来标记当前版本号:

inf.AddValue("VERSION”,CURRENT_VERSION);

//写入其他元素……

)

条款3:操作符is或as优于强制转型19

使用const较之于使用readonly的唯一好处就是性能:使用已知常量值的代码效率要比访问readonly值的代码效

率稍好一点。但是这其中的效率提升是非常小的,大家应该和其所失去的灵活性进行一番权衡比较。在打算放弃

灵活性之前,一定要对两者的性能差别做一个评测。

综上所述,只有当某些情况要求变量的值必须在编译时可用,才应该考虑使用const,例如:特性(attribute)类

的参数,枚举定义,以及某些不随组件版本变化而改变的值。否则,对于其他任何情况,都应该优先选择readonly

常量,从而获得其所具有的灵活性。

条款3:操作符is或as优于强制转型

C#是一门强类型语言。一般情况下,我们最好避免将一个类型强制转换为其他类型。但是,有时候运行时类型检

查是无法避免的。相信大家都写过很多以System.Object类型为参数的函数,因为.NET框架预先为我们定义了这

些函数的签名。在这些函数内部,我们经常要把那些参数向下转型为其他类型,或者是类,或者是接口。对于这种

转型,我们通常有两种选择:使用as操作符,或者使用传统C风格的强制转型。另外还有一种比较保险的做法:

先使用is来做一个转换测试,然后再使用as操作符或者强制转型。

正确的选择应该是尽可能地使用as操作符,因为它比强制转型要安全,而且在运行时层面也有比较好的效率。需

要注意的是,as和is操作符都不执行任何用户自定义的转换。只有当运行时类型与目标转换类型匹配时,它们才

会转换成功。它们永远不会在转换过程中构造新的对象。

我们来看一个例子。假如需要将一个任意的对象转换为一个MyType的实例。我们可能会像下面这样来做:

objecto=Factory.GetObject();

//第一个版本:

MyTypet=oasMyType;

if(t!=null)

//处理t,t现在的类型为MyType。

}else

(

〃报告转型失败。

)

或者,也可以像下面这样来做:

objecto=Factory.GetObject();

//第二个版本:

try(

MyTypet;

t=(MyType)o;

if(t!=null)

(

//处理t,t现在的类型为MyTypeo

}else

(

//报告空引用失败。

)

}catch

(

//报告转型失败。

)

相信大家都同意第•个版本的转型代码更简单,也更容易阅读。其中没有添加额外的try/catch语句,因此也就

避免了其带来的负担。注意,第二个版本中除了要捕捉异常外,还要对null的情况进行检查,因为如果。本来就

是null,那么强制转型可以将它转换成任何引用类型。但如果是as操作符,且被转换对象为null,那么执行结果

将返回null。因此,如果使用强制转型,我们既要检查其是否为null,还要捕捉异常。如果使用as操作符,我

们只需要检查返回的引用是否为null就可以了。

cast和as操作符之间最大的区别就在于如何处理用户自定义的转换。操作符as和is都只检查被转换对象的运行

时类型,并不执行其他的操作。如果被转换对象的运行时类型既不是所转换的目标类型,也不是其派生类型,那

么转型将告失败。但是,强制转型则会使用转换操作符来执行转型操作,这包括任何内建的数值转换。例如,将

一个long类型强制转换为一个short类型将会导致部分信息丢失。

条款3:操作符is或as优于强制转型21

在我们使用用户自定义的转换时,也会有同样的问题,来看下面的代码:

publicclassSecondType

(

privateMyType_value;

//忽略其他细节。

//转换操作符。

//将SecondType转换为MyType,参见条款29。[4]

publicstaticimplicitoperator

MyType(SecondTypet)

(

returnt._value;

)

)

假设下面第一行代码中的Factory.GetObject()返回的是一个SecondType对象:

objecto=Factory.GetObject();

//o为一个SecondType:

MyTypet=oasMyType;//转型失败,o的类型不是MyType。

if(t!=null)

(

//处理t,t现在的类型为MyType。

}else

(

//报告转型失败。

)

//第二个版本:

try(

MyTypetl;

tl=(MyType)o;//转型失败,。的类型不是MyType。

if(tl!=null)

(

//处理tl,tl现在的类型为MyType。

}else

(

//报告空引用失败。

}

}catch

(

//报告转型失败。

)

两个版本的转型操作都失败了。大家应该还记得我前面说过强制转型会执行用户自定义的转换,有读者据此认为

强制转型的那个版本会成功。这么想本身没有错误,只是编译器在产生代码时依据的是对象。的编译时类型。编

译器对于。的运行时类型一无所知——编译器只知道。的类型是System.Object。因此编译器只会检查是否存在将

System.Object转换为MyType的用户自定义转换。它会到System.Object类型和MyType类型的定义中去做这样的

检查。由于没有找到任何用户自定义转换,编译器将产生代码来检查。的运行时类型,并将其和MyType进行比对。

由于。的运行时类型为SecondType,因此转型将告失败。编译器不会检查在。的运行时类型SecondType和MyType

之间是否存在用户自定义的转换。

当然,如果将上述代码做如下修改,转换就会成功执行:

objecto=Factory.GetObject();

//第三个版本:

SecondTypest=oasSecondType;

try(

MyTypet;

t=(MyType)st;

if(t!=null)

(

//处理t,t现在的类型为MyTypeo

}else

(

//报告空引用失败。

)

}catch

(

条款3:操作符is或as优于强制转型24

II报告转型失败。

)

在正式的开发中,我们绝不能写如此丑陋的代码,但它却向我们揭示了问题的所在。虽然大家永远都不可能像上面

那样写代码,但可以使用一个以System.Object类型为参数的函数,让该函数在内部执行正确的转换。

objecto=Factory.GetObject();

DoStuffWithObject(o);

privatevoidDoStuffWithObject(objecto2)

I

try(

MyTypet;

t=(MyType)o2;//转型失败,o的类型不是MyType

if(t!=null)

(

//处理t,t现在的类型为MyType。

}else

(

//报告空引用失败。

)

}catch

(

//报告转型失败。

)

)

记住,用户自定义的转换操作符只作用于对象的编译时类型,而非运行时类型上。至于。2的运行时类型和MyType

之间是否存在转换,并不重要。事实上,编译器对此并不了解,也不关心。对于下面的语句,如果st的声明类型

不同,会有不同的行为:

t=(MyType)st;

但对于下面的语句,不管st的声明类型是什么,都会产生同样的结果回。因此,我们说as操作符要优于强制转

型——它的转型结果相对比较一致。

但如果as操作符两边的类型没有继承关系,即使存在用户自定义转换操作符,也会产生编译时错误。例如,下面

的语句:

t=stasMyType;

我们已经知道在转型的时候应该尽可能地使用as操作符。下面我们来谈谈一些不能使用as操作符的情况。首先,

as操作符不能应用于值类型。例如,下面的代码编译的时候就会报错:

objecto=Factory.GetValue();

inti=oasint;//不能通过编译。

这是因为int是一个值类型,所以不可以为nullo如果。不是一个整数,那这个i里面还能存放什么呢?存入的任

何值都必须是有效的整数,所以as不能和值类型一起使用。那就只能使用强制转型了:

objecto=Factory.GetValue();

inti=0;

try(

i=(int)o:

}catch

(

i=0;

)

但是,我们也并非只能这样。我们还可以使用is语句来避免其中对异常的检查或者强制转型:

objecto=Factory.GetValue();

inti=0;

if(oisint)

i=(int)o;

如果。是某个其他可以转换为int的类型,例如double,那么is操作符将返回false。如果o的值为null,is操

作符也将返回false。

只有当我们不能使用as操作符来进行类型转换时,才应该使用is操作符。否则,使用is将会带来代码的冗余:

//正确,但是冗余:

objecto=Factory.GetObject();

MyTypet=null;

条款3:操作符is或as优于强制转型26

if(oisMyType)

t=oasMyType;

上面的代码和下面的代码事实上是一样的:

//正确,但是冗余:

objecto=Factory.GetObject();

MyTypet=null;

if((oasMyType)!=null)

t=oasMyType;

这种做法显然既不高效,也显得冗余。如果我们打算使用as来做转型,那么再使用is检查就没有必要了。直接将

as操作符的运算结果和null进行比对就可以了,这样比较简单。

既然我们已经明白了is操作符、as操作符和强制转型之间的差别,那么大家猜猜看foreach循环语句中使用的是

哪个操作符来执行类型转换呢?

publicvoidUseCollection(IEnumerabletheCollection)

I

foreach(MyTypetintheCollection)

t.DoStuff();

)

答案是强制转型。事实匕下面的代码和上面foreach语句编译后的结果是•样的:

publicvoidUseCollection(IEnumerabletheCollection)

I

IEnumeratorit=theCollection.GetEnumerator();

while(it.MoveNext())

(

MyTypet-(MyType)it.Current;

t.DoStuff();

)

)

之所以使用强制转型,是因为foreach语句需要同时支持值类型和引用类型。无论转换的目标类型是什么,foreach

语句都可以展现相同的行为。但是,由于使用的是强制转型,foreach语句可能产生BadCastException异常⑥。

由于【Enumerator.Current返回的是System.Object,而Object中又没有定义任何的转换操作符,因此转换操作

符就不必考虑了。如果集合中是一组SecondType对象,那么运用在UseCollection()函数中将会出现转型失败,

因为foreach语句使用的是强制转型,而强制转型并不关心集合元素的运行时类型。它只检查在System.Object

类(山lEnumerator.Current返回的类型)和循环变量的声明类型MyType之间是否存在转换。

最后,有时候我们可能想知道一个对象的确切类型,而并不关心它是否可以转换为另一种类型。如果一个类型继

承自另一个类型,那么is操作符将返回true。使用System.Object的GetType()方法,可以得到一个对象的运行

时类型。利用该方法可以对类型进行比is或as更为严格的测试,因为我们可以拿它所返回的对象的类型和一个具

体的类型做对比。

再来看下面的函数:

publicvoidUseCollection(IEnumerabletheCollection)

foreach(MyTypetintheCollection)

t.DoStuff();

)

如果创建了一个继承自MyType的类NewType,那便可以将一组NewType对象集合应用在UseCollection函数中。

publicclassNewType:MyType

(

//忽略实现细节。

)

如果我们打算编写一个函数来处理所有与MyType类型兼容的实例对象,那么UseCollection函数所展示的做法就

挺好。但如果打算编写的函数只处理运行时类型为MyType的对象,那就应该使用GetType()方法来对类型做精确

的测试。我们可以将这种测试放在foreach循环中。运行时类型测试最常用的地方就是相等判断(参见条款9)。

对于绝大多数其他的情况,as和is操作符提供的.isinst比较山在语义上都是正确的。

条款4:使用Conditional特性代替#if条件编译27

好的面向对象实践一般都告诫我们要避免转型,但有时候我们别无选择。不能避免转型时,我们应该尽可能地使用

C#语言中提供的as和is操作符来更清晰地表达意图。不同的转型方式有不同的规则,is和as操作符绝大多数情

况下都能满足我们的要求,只有当被测试的对象是正确的类型时,它们才会成功。一般情况下不要使用强制转型,

因为它可能会带来意想不到的负面效应,而且成功或者失败往往在我们的预料之外。

#if/#endif条件编译常用来由同一份源代码生成不同的结果文件,最常见的有debug版和release版。但是,这

些工具在具体应用中并不是非常得心应手,因为它们太容易被滥用了,使用它们创建的代码通常都比较难理解,

且难以调试。C#语言的设计者们对这种问题的解决方案是创建更好的工具,以达到为不同环境创建不同机器码的

目的。C#为此添加了一个Conditional特性,该特性可以标示出某种环境设置下某个方法是否应该被调用。使用这

种方式来描述条件编译要比#if/#endif更加清晰。山于编译器理解Conditional特性,所以它可以在Conditional

特性被应用时对代码做更好的验证。Conditional特性应用在方法这一层次上,因此它要求我们将条件代码以方法

为单位来表达。当需要创建条件代码块时,我们应该使用Conditional特性来代替传统的#if/#endif。

大多数程序老手都使用过条件编译来检查对象的前置条件和后置条件。例如,编写一个私有方法来检查所有类与对

象的不变式(invariant)回,然后将这样的方法进行条件编译,从而让其只出现在debug版本的程序中。

privatevoidCheckState()

(

//老式的做法:

#ifDEBUG

Trace.WriteLine("EnteringCheckStateforPerson");

//获取正在被调用函数的名称:

stringmethodName=

newStackTrace().GetFrame(1).GetMethod().Name;

Debug.Assert(_lastName!=null,

methodName,

“LastNamecannotbenull");

Debug.Assert(_lastName.Length>0,

methodName,

“LastNamecannotbeblank");

Debug.Assert(_firstName!=null,

methodName,

“FirstNamecannotbenull");

Debug.Assert(_firstName.Length>0,

methodName,

“FirstNamecannotbeblank");

Trace.WriteLine("ExitingCheckStateforPerson");

#endif

)

条件编译烟£和#前011£使得最终release版本中的CheckState()成为一个空方法,但它在release版和debug版

中都将得到调用。虽然在release版中,CheckState()什么也不做,但是我们必须为方法的加载、JIT编译和调用

付出成本。

就正确性而言,这种做法一般没什么问题,但有时候还是可能会在release版本中导致一些诡异的bug。下面的代

码展示了使用折f和#前选£条件编译时可能常犯的错误:

publicvoidFunc()

(

stringmsg=null;

#ifDEBUG

msg=GetDiagnostics();

ttendif

Console.WriteLine(msg);

)

上面的代码在debug版本中运行得很好,但是放到release版本中就会输出一个空行。输出一个空行本身没有什

么,但这毕竟不是我们本来的意图。我们自己搞糟的事情,编译器也帮不上什么忙,因为我们把属于程序主逻辑

的代码和条件编译代码混在•起了。在源代码中随意地使用#ifqD#endif将使我们很难诊断不同版本间的行为差别。

C#为此提出了一种更好的选择:Conditional特性。使用Conditional特性,我们可以将一些函数隔离出来,使得

它们只有在定义了某些环境变量或者设置了某个值之后才能发挥作用oConditional特性最常用的地方就是将代码

改编为调试语句。.NET框架已经为此提供了相关的功能支持。下面的代码展示了Conditional特性的工作原理,

以及适用场合。

构建Person对象时,我们一般会添加如下的方法来验证对象的不变式:

privatevoidCheckState()

(

//获取正在被调用函数的名称:

stringmethodName=

newStackTrace().GetFrame(1).GetMethod().Name;

Trace.WriteLine(''EnteringCheckStateforPerson:;

Trace.Write(z,\tcalledby〃);

Trace.WriteLine(methodName);

Debug.Assert(_lastName!=null,

methodName,

“LastNamecannotbenull");

Debug.Assert(_lastName.Length>0,

methodName,

“LastNamecannotbeblank");

Debug.Assert(_firstName!=null,

methodName,

“FirstNamecannotbenull");

Debug.Assert(_firstName.Length>0,

methodName,

“FirstNamecannotbeblank");

Trace.WriteLine("ExitingCheckStateforPerson");

)

有些读者可能对上面代码中的一些库函数还不够熟悉,我们来简单介绍一下。StackTrace类使用反射(reflection,

参见条款43)来获取当前正被调用的方法名。其代价相当高,但它可以极大地简化我们的工作,例如帮助我们获

取有关程序流程的信息。在上面的代码中,使用它,我们便可以得到正被调用的方法名称为CheckState。其余的方

法在另外两个类中,分别为System.Diagnostics.Debug和System.Diagnostics.Trace。Debug.Assert方法用于

测试某个条件,如果该条件错误,程序将被终止,其他参数定义的消息也将被打印出来。Trace.WriteLine方法则

会把诊断信息打印到调试控制台匕因此,如果有Person对象状态无效,CheckState方法将会显示信息,并终止

程序。我们可以将其作为前置条件和后置条件,在所有的公有方法和受保护方法中调用它。

publicstringLastName

(

get

(

CheckState();

return_lastName;

)

set

(

CheckState();

_lastName=value;

CheckState();

)

)

当首次试图将LastName属性设置为空字符串或者null时,CheckState将引发一个断言错误。这样我们就会修正

set访问器以检查传递给LastName的参数。这正是我们想要的功能。

但在每个公有函数中都做这样的额外检查显然比较浪费时间,我们可能只希望其出现在调试版本中。这就需要

Conditional特性了:

[Conditional("DEBUG")]

privatevoidCheckState()

(

//代码保持不变。

)

应用了Conditional特性之后,C#编译器只有在检测到DEBUG环境变量时,才会产生对CheckState方法的调用。

Conditional特性不会影响CheckState。方法的编译,它只会影响对该方法的调用。如果定义有DEBUG符号,上

面的LastName属性将变为如下的代码:

publicstringLastName

(

get

(

CheckState();

return_lastName;

)

set

(

CheckState();

_lastName=value;

CheckState();

)

)

否则,符得到如下代码:

publicstringLastName

(

get

return_lastName;

)

set

(

_lastName=value;

)

)

无论是否定义有DEBUG符号,CheckState()方法的方法体都维持不变,它都会被C#编译器处理,并生成到结果程

序集中。这个例子其实也向大家展示了C#编译器的编译过程与JIT编译过程之间的区别。这种做法看起来也会带

来一点效率损失,但是其中耗费的成本仅仅是磁盘空间。如果没有被调用,CheckState()方法并不会加载到内存

中并进行JIT编译。将CheckState()方法生成到程序集中产生的影响是非常微不足道的。这种策略耗费很小的性能,

换来的却是灵活性。如果感兴趣的话,大家可以查看.NET框架类库中的Debug类来对此获得更深的理解。在每个

安装有.NET框架的机器上,System,dll程序集中都包含有Debug类中所有方法的代码。当调用这些方法的代码被

编译时,系统环境变量将决定这些方法是否被调用。

我们创建的方法也可以依赖于多个环境变量。当我们应用多个Conditional特性时,它们之间的组合关系将为“或

(OR)”。例如,下面的CheckState方法被调用的条件为定义有DEBUG或者TRACE环境变量:

[Conditional("DEBUG"),

Conditional("TRACE")]

privatevoid

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论