人家叫我宣传 然后就送我微信免费送手机付运费 运费要36元 是不是真的

&&国之画&&&& &&&&&&
&& &&&&&&&&&&&&&&&&&&&&
鲁ICP备号-4
打开技术之扣,分享程序人生!《C#高效编程:改进C#代码的50个行之有效的办法(第2版)》(人民邮电出版社)【摘要_书评_试读】- 蔚蓝网
共有图书2939794种
您的浏览历史
顾客评分:
已有0人评论
字数:370000&&&&&
页数:268&&&&&
开本:16开&&&&&
包装:平装
十载演进,C#日渐强大,新增了泛型、函数式编程概念。以及对静态类型和动态类型的支持。而大量新兴编程习惯背后,却是种种具有迷惑性的“陷阱”,Bill Wagner将以此书帮您避免C#语言和.NET环境中的“陷阱”。
本书充分挖掘了C#4.0的强大功能,并给出了简明、精确表达设计的50个实用方法。书中用大量代码示例,以50个条目条分缕析,为实际应用给出了清晰、专业、实用的建议,阐述’TC#语言和.NET框架的方方面面,包括类型、资源管理、动态类型和多核支持。
本书围绕C#语言元素、.NET资源管理、使用C#表达设计、创建二进制组件和使用框架等主题,针对50个常见问题给出了详实的解决方案,并就提升编程效率给出了合理建议。书中内容共分6章,分别讲述了实际编程中不可或缺的习惯用法,如何更好地配合开发环境以期在优化之前解决问题,如何用C#语言良好地表达设计意图,BCL、Parallel Task Library的最常见用法和使用框架过程中常用的技巧,如何在C#中使用动态特性,以及一些对创建强壮、易于维护的程序来说非常重要的难以归类的主题。
本书适合所有C#程序员,也可供高等院校相关专业师生参考。
C#语言习惯
使用属性而不是可访问的数据成员
用运行时常量(readonly)而不是编译期常量(const)
推荐使用is或as操作符而不是强制类型转换
使用Conditional特性而不是#if条件编译
为类型提供ToString()方法
理解几个等同性判断之间的关系
理解GetHashCode()的陷阱
推荐使用查询语法而不是循环
避免在API中使用转换操作符
使用可选参数减少方法重载的数量
理解短小方法的优势第2章
.NET资源管理
推荐使用成员初始化器而不是赋值语句
正确地初始化静态成员变量
尽量减少重复的初始化逻辑
使用using和try/finally清理资源
避免创建非必要的对象
实现标准的销毁模式
区分值类型和引用类型
保证0为值类型的有效状态
保证值类型的常量性和原子性第3章
使用C#表达设计
限制类型的可见性
通过定义并实现接口替代继承
理解接口方法和虚方法的区别
用委托实现回调
用事件模式实现通知
避免返回对内部类对象的引用
让类型支持序列化
提供粗粒度的因特网服务API
支持泛型协变和逆变第4章
使用覆写而不是事件处理函数
使用IComparable和IComparer实现顺序关系
避免使用ICloneable接口
仅用new修饰符处理基类更新
避免重载基类中定义的方法
PLINQ如何实现并行算法
理解PLINQ在I/O密集场景中的应用
注意并行算法中的异常第5章
C#中的动态编程
理解动态类型的优劣
使用动态类型表达泛型类型参数的运行时类型
将接受匿名类型的参数声明为dynamic
用DynamicObject或IDynamicMetaObjectProvider实现数据驱动的动态类型
如何使用表达式API
使用表达式将延迟绑定转换为预先绑定
尽量减少在公有API中使用动态对象第6章
尽量减少装箱和拆箱
为应用程序创建专门的异常类
使用强异常安全保证
尽量使用安全的代码
实现与CLS兼容的程序集
实现小尺寸、高内聚的程序集
摘要与插图
C#语言习惯
什么程序已经可以正常工作了,还要继续修改呢?答案就是我们还能让程序变得更好。如果你总是墨守成规,那么将永远体会不到新技术带来的优势。对于C#这种和我们已经熟悉的语言(如C++或Java等)有很多类似之处的新语言,情况更是如此。C#也是一种用大括号组织代码块的语言,因此人们很容易重拾他们熟悉的习惯。但这会阻碍你学到C#的种种精妙之处。自2001年第一个正式版本以来,C#一直在不断改进。其第一个版本与C++和Java尚有许多共同之处,但如今已渐行渐远。若你是从其他语言转到C#。那么应掌握必要的C#语言习惯,让其更好地配合你的工作。本章将讨论那些在C#中应该改变的旧习惯,以及与其对应的推荐的新做法。
条目1 使用属性而不是可访问的数据成员
属性一直是C#语言中的一等公民。自1.0版本以来,C#对属性进行了一系列的增强,让其表达能力不断提高。你甚至可以为setter和getter指定不同的访问权限。隐式属性也极大降低了声明属性时的工作量,不会比声明数据成员麻烦多少。若你仍然在类型中声明公有成员,或是仍在手工编写set或get之类的方法,那么快停下来吧。属性允许将数据成员作为公共接口的一部分暴露出去,同时仍旧提供面向对象环境下所需要的封装。属性这个语言元素可以让你像访问数据成员一样使用,但其底层依旧使用方法实现。
类型的某些成员确实非常适合作为数据,例如某个客户的名称,某个点的x、y坐标或上一年度的收入等。而属性则让你可以创建出类似于数据访问,但实际上却是方法调用的接口,自然也可以享受到方法调用的所有好处。客户代码访问属性时,就像是在访问公有的字段。不过其底层使用方法实现,其中可以自由定义属性访问器的行为。
.NET Framework假设你会对公有数据成员使用属性。实际上,.NET Framework中的数据绑定类仅支持属性,而不支持公有数据成员。对于所有的数据绑定类库均是如此,包括WPF、Windows Forms和Silverlight。数据绑定会将某个对象的一个属性和某个用户界面控件相互关联起来。数据绑定机制将使用反射来找到类型中的特定属性:
这段代码将textBoxCity控件的Text属性绑定到了address对象的City属性上。公有的数据成员并不推荐使用,因此Framework Class Library设计器也不支持其实现绑定。这样的设计也保证了你必须选择合适的面向对象技术。
确实,数据绑定只是用在用户界面逻辑中会使用到的类中。但这并不意味着属性仅应该用在UI逻辑中,其他类和结构中也应使用属性。在日后产生新的需求或行为时,属性更易于修改。例如,你会很快有这样的想法,客户对象不应该有空白的名称。若你使用了公有属性来封装Name,那么只要修改一处即可。
若是使用了公有的数据成员,那么就需要查找每一处设置客户名称的代码并逐一修复。这将花费大量的时间。
因为属性是使用方法来实现的,所以添加多线程支持也非常简单。很容易即可在属性的get和set访问器中作出如下的修改,从而支持对数据的同步访问:
属性可以拥有方法的所有语言特性。例如,属性可以为虚的(virtual):
注意,上述例子中使用了C# 3.0中的隐式属性语法。使用属性来封装私有字段是一个常用的模式。通常而言,我们并不需要验证属性的getter或setter逻辑。因为语言本身提供了简化的隐式属性语法,力求尽量降低开发人员的输入工作,即将一个简单的字段暴露成属性。编译器将为你创建一个私有的成员字段,并自动生成最简单的get和set访问器的逻辑。
你还可以将属性声明为抽象的(abstract),以类似隐式属性语法的形式将其定义在接口中。下面的例子就将属性定义在了一个泛型接口中。需要注意的是,虽然其语法和隐式属性完全相同,但是编译器却不会自动地生成任何实现。接口只是定义了一个契约,强制所有实现了该接口的类型都必须满足。
属性是一种全功能的、第一等的语言元素,能够以方法调用的形式访问或修改内部数据。成员函数中可以实现的功能均可在属性中实现。
属性的访问器将作为两个独立的方法编译到你的类型中。在C#中,你可以为get和set访问器制定不同的访问权限。这样即可更精妙地控制作为属性暴露出来的数据成员的可见性:
上述属性语法的表达含义远远超出了简单数据字段的范畴。若类型需要包含并暴露出可索引的项目,那么可以使用索引器(即支持参数的属性)。若想返回序列中的项,创建一个属性会是个不错的做法:
索引器和单一条目属性有着同样的语言支持:它们都是作为方法实现的,因此可以在索引器内部实现任意的验证或计算逻辑。索引器也可为虚的或抽象的,可声明在接口中,可以为只读或读写。一维且使用数字作为参数的索引器也可参与数据绑定。使用非整数参数的索引器可用来定义图和字典:
C#中支持多维数组,类似地,我们也可以创建多维索引器,每一个维度上可以使用同样或不同的类型:
需要注意的是,所有的索引器都使用this关键字声明。C#不支持为索引器命名。因此,类型中每个不同的索引器都必须有不同的参数列表,以免混淆。几乎属性上的所有特性都能应用到索引器上。索引器也可为虚的或抽象的,可以对setter和getter给出不同的访问限制,不过却不能像属性那样创建隐式索引器。
属性的功能很强大,是个不错的改进。但你是不是还在想能不能先用数据成员来实现,而在稍后需要其他各种功能的时候再改成属性呢?这看似是个不错的策略,不过实际上却行不通。考虑如下这个类的定义:
这个类描述一个客户(Customer),包含了一个名称(Name)。你可以使用熟悉的成员表示方式获取或设置该名称:
看似简单直观,你也会认为若是日后将Name改成属性,那么代码也可以无需修改保持正常。但这个答案并不是完全正确的。属性仅仅是访问时类似于数据成员,这是语法所实现的目的。不过属性并不是数据,属性的访问和数据的访问将会生成不同的MSIL(Microsoft Intermediate Language,微软中间语言)指令。
虽然属性和数据成员在源代码层次上是兼容的,不过在二进制层面上却大相径庭。这也就意味着,若将某个公有的数据成员改成了与之等同的共有属性,那么就必须重新编译所有用到该公有数据成员的代码。C#把二进制程序集作为一等公民看待。该语言本身的一个目标就是支持发布某个单一程序集时,不需要更新整个的应用程序。而这个将数据成员改为属性的简单操作却破坏掉了二进制兼容型,也就会让更新单一程序集变得非常困难。
若是查看属性生成的IL,那么你或许会想比较一下属性和数据成员的性能。属性当然不会比数据成员访问快,不过也不会比其慢多少。JIT编译器将内联一些方法调用,包括属性访问器。当JIT编译器内联了属性访问器时,数据成员和属性的访问效率即可持平。即使某个属性访问器没有被内联,其性能差距也实在是微乎其微,仅仅一次函数调用之别而已。只有在某些极端情况下,二者的差距才会有所影响。
在调用方来看,属性虽然是方法,但它和数据却有着类似的概念。这会使你的调用者对属性有着一些潜意识的认识。例如,调用者会把属性访问当成是数据的访问。不管怎样,二者看上去很像。属性访问器应该满足这些潜意识的预期。get访问器不应该有可被观察到的副作用。set访问器会修改状态,用户应该可以看到调用后带来的改变。
调用者也会对属性访问器的性能有着一定的预期。属性的访问就像是访问一个数据字段,因此不会与访问数据有太过明显的性能差别。属性访问器不应该执行长时间的计算,或进行跨应用程序的调用(例如执行数据库查询等),或是其他任何与调用者期待不符的耗时操作。
无论何时需要在类型的公有或保护接口中暴露数据,都应该使用属性。你也应该使用索引器来暴露序列或字典。所有的数据成员都应该是私有的,没有任何例外。这样你就立即得到了数据绑定的支持,也便于日后对方法实现的各种修改。对于将任何变量封装到一个属性所需的额外输入工作其实不会占用太多时间,而日后若是需要使用属性来更正设计,则会花去大量的时间。现在多投入一点点,换来的是今后维护时的更加游刃有余。
条目2 用运行时常量(readonly)而不是编译期常量(const)
C#有两种类型的常量:编译期常量和运行时常量。二者有着截然不同的行为,使用不当将会带来性能上或正确性上的问题。这两个问题最好都不要发生,不过若难以同时避免的话,那么一个略微慢一些但能保证正确的程序则要好过一个快速但不能正常工作的程序。考虑到这些,你应该尽量使用运行时常量,而不是编译期常量。虽然编译期常量略微快一些,但是却没有运行时常量那么灵活。应仅仅在那些性能异常敏感,且常量的值在各个版本之间绝对不会变化时,再使用编译期常量。
运行时常量使用readonly关键字声明,编译期常量则使用const关键字声明:
上述代码在类或struct的范围内演示了两种常量。编译期常量也可声明在方法中,而只读的运行时常量却不能声明在方法中。
编译期常量与运行时常量行为的不同之处在于对它们的访问方式不同。编译期常量的值是在目标代码中进行替换的。以下构造:
将与如下代码编译成同样的IL:
运行时常量将在运行时求值。引用运行时常量生成的IL将引用到readonly的变量,而不是变量的值。
这个差别会带来几个限制,会影响到选用哪种类型的常量。编译期常量仅能用于基本类型(内建的整数和浮点类型)、枚举或字符串。只有这些类型才允许我们在初始化器中指定有意义的常量值。在代码编译后得到的IL代码中,只有这些常量可直接被替换为它们的字面值。例如,下面的代码就不会通过编译。即使要初始化的常量类型属于值类型,也无法在C#中使用new操作符来初始化编译期常量:
编译期常量(const)仅能用于数字和字符串。运行时常量(readonly)也是一种常量,因为在构造函数执行后不能被再次修改。二者的区别在于,只读的值将在运行时给出,这自然会带来更好的灵活性。例如,运行时常量可以为任意类型。运行时常量必须在构造函数或初始化器中初始化。你可以让某个readonly值为一个DataTime结构,而不能指定某个const为DataTime。
你可以用readonly值保存实例常量,为类的每个实例存放不同的值。而编译期常量就是静态的常量。
二者最重要的区别在于,readonly值是在运行时解析的。引用一个readonly常量时生成的IL引用的是readonly变量,而不是其值。这一点将会对日后的维护产生深远的影响。编译期常量将生成同样的IL,就像直接在代码中给出数字一样,即使是跨程序集也是如此,即使另一个程序集中引用了某个程序集中的某个常量,相应常量也会被直接替换成这个值。
编译期常量与运行时常量的求值方式将会影响运行时的兼容性。假设我们在一个名为Infrastructure 的程序集中分别定义了一个const字段和一个readonly字段:
在另一个程序集中,我们使用了这两个值:
执行该程序,输出如下:
过了一段时间,我们更新了Infrastructure程序集,做出了如下修改:
随后,在分发Infrastructure程序集时,并没有重新编译整个应用程序的所有程序集。我们期待的是如下的输出:
但实际上,你却看不到任何输出。因为现在那个循环语句将使用105作为它的起始值,使用10作为它的结束条件。C#编译器在第一次编译 Application程序集时,将其中的EndValue替换成了它对应的常量值10。而对于StartValue来说,因为它被声明为readonly,所以对其求值会发生在运行时。因此,Application程序集在没有被重新编译的情况下,使用了新的StartValue值。若想修改所有使用readonly常量的客户代码的行为,只要简单地更新一个Infrastructure 程序集就够了。相反,更改一个公有的编译期常量的值应该被看做是对类型接口的修改,你必须重新编译所有引用该常量的代码。而更改只读常量的值却仅仅算作是对类型实现的修改,它与其客户代码在二进制层次上是兼容的。
不过,有时你确实需要让某个值在编译时确定。例如,考虑用一系列常量来标出对象的不同版本的序列化形式(参见条目27),其中这一组常量用来区分不同版本的对象。这时,用来标记特定版本号的值就应该采用编译期常量,因为它们永远都不会改变。而标记当前版本号的值就应该采用运行时常量,因为它的值会随着每个不同版本的发布而变化。
使用运行时常量在每个存盘文件中保存当前的版本:
相比之下,const最终优于readonly的地方就是性能。使用已知常量值要比访问readonly值略微高效一些。不过这其中的效率提升可以说是微乎其微,而这样做却将降低很多的灵活性。若是你最终还是计划要放弃灵活性,那么在决定之前一定要对两者的性能差别做一个实际的测试。
在使用具名和可选参数时,你也会遇到同样的运行时/编译期常量的取舍。可选参数的默认值将被放置于调用端,就像编译期常量(用const声明)的默认值一样。正如选用readonly和const一样,在修改可选参数的值时也必须小心操作(参见条目10)。
在编译期必须得到确定数值时一定要使用const。例如特性(attribute)的参数和枚举的定义等,还有那些在各个版本发布之间不会变化的值。在除此之外的所有情况下,都应尽量选择更加灵活的readonly常量。
条目3 推荐使用is或as操作符而不是强制类型转换
使用C#也就意味着使用了一种强类型的语言。强类型有着诸多的好处,例如,编译器可以帮你找到代码中的类型不一致,应用程序也无需在运行时进行过多的类型检查等。不过在某些时候,运行时类型检查仍是必不可少的。有时你必须编写接受object作为参数的方法,因为框架已经预定义了方法的签名。那么接下来就是将这些object转型成为特定的类型,要么是类,要么是接口。这时我们有两个选择,或者使用as操作符,或者使用强制类型转换。抑或采取更保险一些的做法,首先使用is测试一下转换能否成功,然后再用as或强制类型转换。
正确的做法是,尽可能地使用as操作符,因为相对于强制类型转换来说,as更加安全,也更加高效。不过,as和is操作符都不会执行任何用户自定义的转换,它们仅当运行时类型符合目标类型时才能转换成功,也不会在转换时创建新的对象。
来看一个例子。假如需要将一个任意的对象转换为MyType的一个实例,那么可以这样做:
或者也可以这样:
显然,第一种做法更加简单易读。其中并没有try/catch语句,因此也就避免了其带来的开销。需要注意的是,第二个版本中除了要捕捉异常外,还要对null的情况进行检查,因为null可以被强制转换成任意的引用类型。不过as操作符在转换对象为null时却将返回null。因此,如果使用强制转型,我们需要同时检查其是否为null并捕捉异常。若是使用as操作符,则只需要检查返回的引用是否为null即可。
cast操作符和as操作符之间最大的区别就在于如何处理用户自定义的转换。as和is操作符都只检查被转换对象的运行时类型,并不执行其他操作。若运行时类型既不是所转换的目标类型,也不是其派生类型,那么转型将宣告失败。而强制转型则会使用转换操作符来执行转型操作,包括框架内建的数值转换。注意,将一个long类型强制转换为一个short类型将会导致部分信息丢失。
在转换用户自定义类型时,也会出现同样的问题。例如这个类型:
假设上一段代码中的Factory.GetObject()函数返回了一个SecondType对象:
上述两个版本都将以失败告终。但前面不是提到过强制转型会执行用户自定义的转换吗?这个想法本身没有错误,不过失败的原因是编译器在生成代码时依据的是对象o的编译期类型。编译器对于o的运行时类型一无所知,而仅仅把o当做object。随后,编译器会发现没有用户自定义的将object转换为MyType的代码。编译器会到System.Object类型和MyType类型中再次检查。由于没有找到任何用户自定义转换,编译器将生成代码来检查o的运行时类型是否为MyType。因为o的运行时类型为SecondType,所以转型不能成功进行。在整个过程中,编译器不会检查o的实际运行时类型是否可以转换为MyType。
当然,如果将上述代码做如下修改,即可成功地将SecondType转换为MyType:
永远都别写出如此丑陋的代码,不过这段代码却演示了一个常见的问题。虽然这种做法不推荐,但你确实可以给函数传入object参数,然后让函数在内部执行必要的转换。
需要牢记的是,用户自定义的转换操作符仅能操作于对象的编译期类型,而不是运行时类型。o和MyType之间的运行时转换对能否成功进行自定义转换不会起到任何作用。编译器不了解也不关心这些。根据st声明的类型的不同,下面这条语句有着不同的行为:
而对于下面这条语句,不管st声明成了什么类型,都会得到同样的结果。因此,你应该尽量使用as进行转型,其行为更加一致。实际上,若两个类型之间没有继承关系,却存在一个用户自定义的转换操作符,那么下面的语句将抛出编译错误:
现在,你知道了何时应该使用as,接下来我们来看看何时不该使用as。as操作符并不能配合值类型使用。下面这条语句就无法通过编译:
这是因为int是一个值类型,永远都不可能为null。那么,若o并不是个int的话,应该将i设置为什么int值呢?所有可能的值都是一个合法的整数。因此,此时不能使用as。你仍应该使用强值类型转换。其过程实际上是一个装箱/拆箱的操作(参见条目45):
使用异常来控制程序流程是一个很糟糕的做法(参见条目47)。不过这也并非你的唯一选择。我们可以使用is语句来避免抛出异常或转换:
若o是其他的某个可以转换成int的类型,例如double,那么is操作符将会返回false。若参数为null,那么is操作符总会返回false。
仅当不能使用as进行转换时,才应该使用is操作符。否则,is就是多余的:
前面这段代码和下面的这段实际上是一样的:
这样做并不高效,且有些累赘。若你准备使用as进行类型转换,那么is检查就毫无必要。只需检查返回值是否为null即可,这样更加简单。
既然我们已经明白了is操作符、as操作符和强制类型转换之间的差别,那么你认为foreach循环语句中使用的是哪个操作符来执行类型转换的呢?foreach循环可以操作在非泛型的IEnumerable序列之上,且迭代过程中内建了强制类型转换。(这里应该尽可能地使用类型安全的泛型版本。非泛型版本仅仅是为了兼容历史遗留问题或支持一些延迟绑定场景而存在。)
答案是,foreach使用强制转型将对象转成循环中将要使用的类型。事实上,上面foreach语句生成的结果和下面的手工代码基本等价:
foreach需要使用强制转型来同时支持值类型和引用类型。既然使用了强制转型,那么foreach语句自然会保证同样的行为,无论最终的类型是什么。不过因为其中使用了强制转型,所以foreach循环中可能会抛出InvalidCastException异常。
IEnumerator.Current返回的是System.Object,而Object中又没有定义任何的转换操作符,因此转换操作符不能起到什么作用。而若集合中的是SecondType对象,那么用在UseCollection()函数中将会出现转型失败,就像前面看到的那样。因为foreach语句使用的是强制转型,而强制转型并不关心集合元素的运行时类型。它只检查在System.Object类(即IEnumerator.Current返回的类型)和循环变量的声明类型(这里是MyType)之间是否存在转换。
不过,有时候你可能想知道一个对象的确切类型,而并不仅仅是考虑它是否可以转换为另一种类型。若一个类型继承自目标类型,那么is操作符将返回true。当然,调用System.Object的GetType()方法也能得到一个对象的运行时类型。这个测试方法要比is或as更为严格一些,将返回对象的类型,并和某个特定类型进行比较。
再看一下这个方法:
若你编写一个继承自MyType的NewType类,那么NewType的集合也可以适用于UseCollection方法:
如果你想编写一个方法来处理所有与MyType类型兼容的派生对象,那么这种做法自然不错。而若是只想处理运行时类型为MyType的对象,那么就应该使用该类型做精确的比较。这里可将该比较放在foreach循环中。运行时类型比较最常用的地方就是等同性判断(参见条目6)。对于绝大多数其他的比较,as和is操作符提供的.isinst比较在语义上都是正确的。
.NET BCL(Base Class Library)提供了一个方法来将序列中的每个元素进行转型。对于一个支持传统IEnumerable接口的序列来说,Enumerable.Cast ()方法将转换其中的每个元素:
上述查询将生成与方法调用同样的代码。对于这两种情况,Cast 方法都会把序列中的每个元素转换成目标类型。Enumerable.Cast 方法使用的是传统的强制转型,而不是as操作符。使用老式的强制转型也就意味着Cast 不需要有类约束。这里使用as操作符将带来限制,考虑到这些,BCL选择创建一个唯一的、使用强制转型的方法,而不是给出多个Cast 的实现。你也应该在你的代码中仔细权衡此类问题。若是需要将泛型类型参数中的一个对象转换为指定的类型,那么也需要衡量一下使用类约束与处理转型操作符带来的不一致行为的利弊。
在C# 4.0中,类型系统变得更加复杂——引入了dynamic和运行时类型检查。这是第5章的内容。有很多种方法来根据对对象已知行为的预期来操作对象,而无须了解其是否为特定的类型或实现了特定的接口。稍后你会看到何时应该使用,何时应该避免。
好的面向对象实践一般都告诫我们要避免转型,但有时候你却别无选择。在无法避免转型时,我们应该尽可能地使用C#语言中提供的as和is操作符来更清晰地表达意图。不同的转型方式有不同的规则。is和as操作符在绝大多数情况下都能表达出正确的语义,只有当被测试的对象是正确的类型时才会成功。应尽量选择is和as而不是强制类型转换,因为强制类型转换可能会带来意想不到的负面效应,而且成功或者失败往往在我们的预料之外。
条目4 使用Conditional特性而不是#if条件编译
#if/#endif语句常用来基于同一份源代码生成不同的编译结果,其中最常见的就是debug版和release版。但是,这些工具在实际应用中并不是非常友好,因为它们容易被滥用,其代码也进而难以理解或调试。C#语言设计者考虑到了这个问题,并提供了更好的工具,用来为不同环境编译出不同的机器码。C#为此添加了一个Conditional特性,该特性可以标识出某种环境设置下某个方法是否应该被调用。使用这种方式来描述条件编译要比#if/#endif更加清晰。编译器理解Conditional特性,因此也可以在使用了Conditional特性时更好地验证代码。Conditional特性适用于方法的层面,这将强制我们将条件代码拆分为独立的方法。在需要编写条件代码块时,我们应该使用Conditional特性来代替#if/#endif。
大多数有经验的开发者都曾用过条件编译来检查对象的前置条件和后置条件。例如,编写一个私有方法来检查所有类与对象的状态是否合法,然后将这个方法进行条件编译,让其只出现在debug版本的程序中。
条件编译#if和#endif将会在最终release版本中留下一个名为CheckState()的空方法,但它在release版和debug版中都将被调用。虽然在release版中CheckState()什么也不做,但是方法的加载、JIT编译和调用仍旧有些开销。
这种做法一般没有什么问题,但有时候也会在release版本中引发一些bug。下面的代码就演示了使用条件编译时可能出现的错误:
上面的代码在debug版本中不会有什么问题,但是在release版本中就会输出一个空行。这并不是我们希望看到的。自己分内的事情没有做好,编译器也帮不上什么忙,出现错误的原因是我们把属于程序主逻辑的代码和条件编译代码混在一起了。在源代码中随意地使用#if和#endif将让你很难诊断出不同版本之间的行为差异。
C#为此提出了一种更好的选择:Conditional特性。使用Conditional特性即可将一些函数拆分出来,让其只有在定义了某些环境变量或者设置了某个值之后才能编译并成为类的一部分。Conditional特性最常用的地方就是将一段代码变成调试语句。.NET Framework库已经为此提供了相关的功能支持。下面的代码展示了在.NET Framework 中如何使用调试功能,来演示Conditional特性的使用方法以及适用场合。
在创建Person对象时,我们可以添加如下的方法来检查对象的状态:
或许你对上述代码中的一些库函数还不够熟悉,这里先来简单介绍一下。StackTrace类将使用反射来获取当前正在被调用的方法的名称。其代价相当高,但却可以极大地简化我们的工作,例如生成有关程序流程的信息。在上面的代码中,使用StackTrace即可得到正在被调用的方法名称为CheckState。不过还有一点风险,即调用方法已经被内联。对于这种情况,我们可以让每个调用CheckState()的方法都传入其方法名称(使用MethodBase. GetCurrentMethod())。稍后将会介绍为何这里没有采取这种做法。
其余的方法均定义于System.Diagnostics.Debug或System.Diagnostics.Trace两个类中。Debug.Assert方法用于测试某个断言,如果该断言不满足,程序将被终止,同时将打印出其他参数定义的消息。Trace.WriteLine方法将会把诊断信息输出到调试控制台上。因此,如果有Person对象的状态不合法,CheckState方法将会显示信息,并终止程序。我们可以将这个方法作为前置条件和后置条件,在所有的公有方法和属性中调用。
首次有人尝试将LastName属性设置为空字符串或者null时,CheckState将抛出一个断言错误。这样调用者即可找到问题,并快速修复——这正是我们想要的功能。
但在每个公有的例程中都做这样的额外检查显然比较浪费时间,我们可能只希望其出现在调试版本中。这时即可使用Conditional特性:
应用了Conditional特性之后,C#编译器只有在检测到定义了DEBUG环境变量时才会对CheckState方法进行调用。Conditional特性不会影响对CheckState()方法的编译,它只会影响对该方法的调用。如果定义了DEBUG符号,你会得到如下的代码:
如果没有定义,那么将得到这样的代码:
无论是否定义有DEBUG符号,CheckState()方法的方法体都不变。这个例子其实也演示了C#编译器的编译过程与JIT编译过程之间的区别。无论是否定义了DEBUG环境变量,CheckState()方法都将被编译至程序集中。这种做法看起来也似乎不那么高效,但是其中占用的仅仅是一点点磁盘空间而已。如果没有被调用,CheckState()方法并不会加载到内存中,也不会被JIT编译。将CheckState()方法生成到程序集中产生的影响是非常微不足道的。这种策略用很小的性能降低换来了更高的灵活性。如果感兴趣的话,可以参考.NET Framework类库中的Debug类来深入理解。在每个安装有.NET Framework的机器上,System.dll程序集中都包含有Debug类中所有方法的代码。当调用这些方法的代码被编译时,系统环境变量将决定这些方法是否被调用。使用Conditional特性支持你创建内嵌有调试功能的库,这些调试功能也可以在运行时启用或禁用。
这种方式创建的方法甚至可以依赖于多个环境变量。在应用多个Conditional特性时,它们之间的组合关系将为“或”(OR)。例如,下面的CheckState方法被调用的条件为,要么定义了DEBUG,要么定义了TRACE环境变量:
而若想创建一个使用“与”(AND)关系的构造,则需要自己在源代码中定义预处理符号:
可以看到,若想创建一个依赖于多个环境变量的条件例程,我们不得不回到使用#if的老式做法中去。不过,所有#if都只不过是创建新的符号而已,我们应该避免将可执行代码放在其中。
随后即可按照老式的做法编写CheckState方法:
Conditional特性只可以应用在整个方法上。另外需要注意的是,任何一个使用Conditional特性的方法都只能返回void类型。你不能在方法内的代码块上应用Conditional特性,也不可以在有返回值的方法上应用Conditional特性。为了应用Conditional特性,我们需要将所有的条件性的行为单独放到一个方法中。虽然你仍需要注意那些条件性方法可能给对象状态带来的副作用,但是Conditional特性的隔离策略总归要比#if/#endif好得多。使用#if和#endif代码块,你甚至会不小心错误地删除一些重要的方法调用或者赋值语句。
上面的例子使用了DEBUG或者TRACE这样的预定义符号,但你也可以将其技术扩展为使用自己定义的符号。Conditional特性可以被任何方式定义的符号控制。你可以在编译器命令行、操作系统的环境变量,或者源代码中给出这些符号。
或许你已经注意到了,上面的每个使用了Conditional特性的方法都返回void类型,且不接受任何参数。这个规则我们必须遵守,编译器将强制Conditional方法必须返回void类型,不过你仍可以让该方法接受任意数目的引用类型参数。但这是个不好的做法,有可能会导致一些负面效果。比如,考虑如下一段代码:
这里的SomeMethod是一个Conditional方法:
这样也就会出现一些难以察觉的bug。仅当定义了DEBUG符号时,才会调用SomeMethod()方法。若没有定义的话,那么就不会调用。自然,names.Dequeue()也不会被调用。因为程序不再需要方法的返回值,所以也就不会调用该方法。考虑到这些,所有应用了Conditional特性的方法都不应该接受任何参数。因为用户可以调用某些会产生副作用的方法来得到Conditional方法的参数,但若Conditional方法没有被调用的话,那么这些产生副作用的方法也不会被调用。
综上所述,使用Conditional特性生成的IL要比使用#if/#endif时更有效率。同时,将其限制在函数层面上可以更加清晰地将条件性的代码分离出来,以便进一步保证代码的良好结构。此外,C#编译器也为此提供了良好的支持,从而避免了以前使用#if或#endif时常犯的错误。与预处理指令相比,Conditional特性让我们可以更好地将条件性代码分离开来。
条目5 为类型提供ToString()方法
System.Object.ToString()是.NET环境中最常用的方法之一。你应该为类型的所有使用者提供一个合理的ToString版本,否则使用者就不得不用你的类的一些属性来自行构造出一个人类可读的表示。类型的字符串表示可用来在各种情况下向用户轻松显示对象的相关信息,例如在WPF(Windows Presentation Foundation)控件、Sliverlight控件、Web Forms、控制台输出窗口中。另外,类型的字符串表示还可用于调试环境。因此,我们创建的每一个类型都应该能覆写Object类的ToString()方法。如果创建的是更复杂的类型,那么还应该实现更加完备的IFormattable.ToString()方法。若是没有覆写该方法,或者写得不够好,那么使用者就必须自己想办法修补。
System.Object默认提供的ToString()方法会返回类型的完整名称。此类信息一般没有什么用处,像"System.Drawing.Rect"、"MyNamespace.Point"、"SomeSample.Size"这样的字符串大多都不能直接显示给用户。但若是不覆写Object的ToString()方法的话,用户看到的就会是这些。只要编写一次类,使用者即可永远受用。编写类这样的一点点的工作,就可以让你和别人同时受益。
接下来,看看覆写System.Object.ToString()这个最简单的需求。该方法主要的功能就是为类型提供一个容易理解的、文本方式的表示。例如,考虑下面这个包含三个字段的Customer类:
继承自Object类的ToString()方法将直接返回一个"Customer"字符串。这个字符串实在没有什么用处。即使ToString()方法只用来调试,也应该输出一些更有意义的信息。在覆写的时候应该尽量返回使用者所希望看到的表示。对于Customer类来说,返回其名称是个不错的选择:
即使你不想遵循本条目中的其他建议,也请遵循上面提到的这一点。因为它可以立竿见影地节省所有人的时间。在提供了Customer类的ToString()覆写之后,该类的对象即可更容易地添加到WPF控件、Sliverlight控件、Web Forms控件或者被打印输出。.NET BCL在将对象显示到如组合框、列表框、文本框或其他控件上时使用Object.ToString()的覆写版本。如果我们在Windows Form或者Web Form上创建了一个Customer对象的列表,其文本显示将为Customer的名称,因为System.Console.WriteLine()方法、System.String. Format()方法在内部都调用了ToString()方法。只要.NET BCL需要获取Customer的字符串表示,你的Customer类型都将给出其名称。不过提供一个具有三行代码的方法,即可照顾到所有的这些基本需求。
在C# 3.0中,编译器会为所有的匿名类型创建一个默认的ToString()方法。该默认的ToString()方法将显示对象中的每个属性值。其中,表示序列的属性是LINQ查询结果,将显示出序列中类型的信息,而不是其中的每个值。参见如下代码片段:
除非你为类型给出自定义的ToString()覆写,否则就连编译器生成的匿名类型都会显示出与用户自定义类型相比的更友好的输出。显然,你应该为类型的使用者提供足够的支持,至少不能输给编译器为某个方法作用范围内的某个临时类型提供的表示。
虽然简单的ToString()方法很多时候已经可以满足为用户定义的类型显示文本信息的需求,但有时还会需要功能更强的方法。上述Customer类型有三个字段:名称、收入和联系电话,而前面的System.Tostring覆写仅使用了名称这个字段。我们可以通过实现IFormattable接口来解决这个不足。IFormattable接口包含了一个重载的ToString()方法,它允许我们为类型提供特定格式的信息。当你需要为类型输出不同形式的字符串时,这个接口即可大显身手。Customer类就是一个例子。比如说,你可能想要创建一个报表,在其中以表格的形式列出客户的名称和上一年的收入。IFormattable.ToString()方法允许用户为该类型指定特定格式的字符串输出,其签名包含了一个格式化字符串和一个格式提供器,如下所示:
这里可以使用格式字符串来为类型指定自己的格式。例如使用特定的字符来表示某种格式信息。在Customer类的例子中,我们可以使用n来表示名称,使用r来表示收入,使用p来表示联系电话。不仅如此,还可以指定这些字符的组合形式,例如如下版本的IFormat- table.ToString():
这样,Customer的使用者即可自行定义其想要的输出格式:
一般来说,IFormattable.ToString()的实现会根据具体类型的不同而有所差别,但有些格式化工作无论是哪个类型都需要处理。首先,我们必须支持表示通用格式的"G"。其次,我们必须支持两种形式的空格式,即""和null。这三种格式返回的字符串都必须与Object.ToString()的覆写版本返回的字符串相同。对于每一个实现了IFormattable接口的类型,.NET BCL都会调用IFormattable.ToString(),而非Object.ToString()。.NET BCL通常会用一个null的格式字符串来调用IFormattable.ToString(),只是在一小部分场合会使用"G"来表示通用格式。如果你的类型支持IFormattable接口,但却不支持这些标准格式,那么也就破坏了BCL中的字符串自动转换规则。支持IFormattable是个庞大的工作,你会发现新的需求会很快接踵而来。你永远都无法预料到所有的可能需要的各种格式。因此,应给出一些最有可能会需要的格式,其他的需求就留给使用者去自行实现吧。
IFormattable.ToString()方法的第二个参数是一个实现IFormatProvider接口的对象。该对象允许客户程序提供一些我们无法事先预料的格式化选项。在前面IFormattable. ToString()的实现中,总会有一些用户期望但实际上却没有提供的格式化选项。这也是提供的人类易读输出过程中普遍存在的一个问题。不管你已经支持了多少种格式化选项,用户总有一天还会需要另一种没有被你预料到的格式。这就是上面代码中开始的几行所做的工作,即找到实现IFormatProvider接口的对象,然后将格式化任务交给其中的ICustom- Formatter来完成。
接下来,我们把视角从类的创建者转换到类的使用者上。假设期望的某种格式没有得到支持,例如某些Customer的名称字符数要大于20,这时候我们希望提供字符宽度为50的Customer名称输出。这正是IFormatProvider接口大显身手之处。这里我们需要创建两个类,一个实现IFormatProvider接口,另一个实现ICustomFormatter接口,用于创建自定义的输出格式。IFormatProvider接口中定义有一个方法GetFormat(),将返回一个实现ICustomFormatter接口的对象。ICustomFormatter接口中包含了实际执行格式化操作的方法。下面的代码实现了提供字符宽度为50的Name输出:
这个GetFormat()方法创建了一个实现ICustomFormatter接口的对象。ICustom- Formatter.Format()方法则按照所需要的方式执行实际的格式化输出工作,将对象转换为字符串格式。我们可以为ICustomFormatter.Format()方法定义格式字符串,从而同时在一个例程中指定多种格式化选项。参数 FormatProvider则是来自GetFormat()方法的一个IFormatProvider对象。
若想指定自己的定制格式,需要显式调用string.Format()方法,并传入一个IFormat- Provider对象:
无论一个类是否实现了IFormattable接口,我们都可以为其创建IformatProvider和ICustomFormatter的实现。因此,即使一个类的原作者没有提供合适的ToString(),你仍然可以自行构建并为其提供格式化支持。当然,作为类的外部访问者,我们只能通过访问其中的公有属性和数据成员来构造字符串。虽然编写实现IFormatProvider和ICustomFormatter这两个类需要不少的工作,且其目的仅仅是为了得到文本输出,但是用这种方式来实现自己定义的字符串输出就能保证其会在.NET Framework的各个地方得到支持。
现在,让我们再次回到类的创建者角色上来。覆写Object.ToString()是为类提供字符串表示的最简单方式。每创建一个类型时,你都要提供该字符串表示。因为该字符串表示应该是我们类型的最明显、最常用的一种表示。其输出也不应太过冗长,一般用在控件、HTML页面或其他将显示给用户查看的地方。只有在一些较少的情况下,即希望为类型提供更复杂的输出格式时,才有必要实现IFormattable接口。它以一种标准的方式允许类的用户来定制类的字符串输出。如果我们没有做这些工作,用户就要自行编写自定义格式化器。这会需要更多的代码,因为用户处于类的外部,所以无法访问到对象的内部状态。不过也不要为此太过担心,毕竟发布者无法预料到所有格式。
人们总要通过某种方式获取到类型的信息,而字符串的表示则是最通俗易懂的。因此,我们应该覆写所有类型中的ToString()方法,让其简单明了地输出对象的摘要信息。
条目6 理解几个等同性判断之间的关系
当我们创建自己的类型时(无论是类还是struct),应为类型定义“等同性”的含义。C#提供了4种不同的函数来判断两个对象是否“相等”:
语言本身支持你创建自有版本的这4种方法,但并不意味着我们应该这么做。对于前两个静态函数,我们永远都不应该去重新定义。我们通常需要创建自己的Equals()方法,来为类型定义等同性语义。偶尔需要覆写operator==(),主要是考虑值类型的性能。此外,这4个函数之间也存在一定的关系。当我们改变其中一个时,有可能影响其他几个函数的行为。虽然看起来用4个函数来做等同性判断有些过于复杂,但是不要担心,我们可以简化这个问题。
当然,这4个方法并不是等同性比较的唯一选择。覆写Equals()方法的类型也应该实现IEquatable 。实现值语意(value semantics)的类型同时还应该实现IStructural- Equality接口。这就意味着一共有6种方法来表达等同性。
就像C#中许多复杂的元素一样,这里你需要考虑到同样的事实,即C#允许我们创建两种类型:值类型和引用类型。如果两个引用类型的变量指向的是同一个对象,它们将被认为是“引用相等”。如果两个值类型的变量类型相同,而且包含同样的内容,它们被认为是“值相等”。这也正是等同性判断需要如此多方法的原因。
让我们首先从那两个永远都不必重新定义的静态函数开始。如果两个变量指向同一个对象,也就是它们拥有同样的对象标识(object identity),那么Object.ReferenceEquals()方法将返回true。无论比较的是引用类型还是值类型,该方法判断的依据都是对象标识,而不是对象内容。这也就意味着,如果我们使用ReferenceEquals()来比较两个值类型,其结果将永远返回false。即使我们将一个值类型和它自身进行比较,ReferenceEquals()的返回值仍是false。其原因在于装箱,参见条目45。
我们永远都不应该去重新定义Object.ReferenceEquals()方法,因为它已经完美地完成了所需要完成的工作——判断两个不同变量的对象标识是否相等。
第二个永远都不应该去重新定义的静态函数是Object.Equals()。当你不知道两个变量的运行时类型时,可以使用该方法来判断两个变量是否相等。注意,System.Object是C#中所有类型的最终基类。因此,任何时候我们比较的两个变量都是System.Object的实例。值类型变量和引用类型变量都是如此。那么该方法又将如何判断两个变量是否相等呢?因为该方法并不知道它们的类型,而等同性判断又是依赖类型的。答案很简单,该方法会将判断的具体操作委托给其中一个类型来做。实际上,静态Object.Equals()方法的实现如下所示:
上面代码中引入的这个方法还没有介绍,即Equals()实例方法。稍后会详细介绍这个方法,不过静态Equals()方法的讨论还没有结束。目前而言你需要理解的是,在静态Equals()方法的内部,实际上是通过调用left参数的实例Equals()方法来判断两个对象是否相等的。
和ReferenceEquals()方法一样,你永远都不要去重新定义或覆写静态的Object. Equals()方法,因为它也已经很好地完成了其应该做的工作,即在不知道两个对象的运行时类型时,判断二者是否相等。因为静态的Equals()方法会将判断的工作委托给带left参数的实例Equals()方法来执行,所以它会使用left参数的类型中定义的规则来进行等同性判断。
现在,你应该理解了为什么我们永远都不需要重新定义静态ReferenceEquals()和Equals()方法。下面我们来讨论那些可以被覆写的方法。但首先,我们先来简要谈谈等同性关系在数学上的定义。对于等同性判断,需要保证的是我们给出的定义和实现与其他开发者的期望保持一致。这意味着我们需要牢记等同性在数学方面的几个要点:自反(reflexive)、对称(symmetric)和可传递(transitive)。自反表示任何对象都和其自身相等。无论a是什么类型,a==a都应该返回true。对称意味着等同性判断时的顺序是无关紧要的,也就是说若a==b返回true,那么b==a也必然返回true;若a==b返回false,那么b==a也必然返回false。可传递的含义是,若a==b且b==c都返回true,那么a==c也必然返回true。
下面我们来介绍Object.Equals()实例函数,包括应该何时以及如何覆写。当Equals()方法的默认行为与我们的类型要求不一致时,自然需要覆写。Object.Equals()方法默认使用对象标识判断,即比较两个对象是否引用相等,默认的Object.Equals()行为和Object. ReferenceEquals()完全一致。但是值类型却是个例外,System.ValueType确实覆写了Object.Equals()方法。记住,ValueType是所有值类型(我们使用struct关键字创建的类型)的基类型。如果两个值类型变量的类型相同,并且内容一致,这两个变量才被认为相等。ValueType.Equals()方法实现了这种行为。不过ValueType.Equals()方法提供的实现效率并不高。由于ValueType.Equals()是所有值类型的基类,为了提供正确的行为,它必须能够在不知道对象运行时类型的情况下,比较其派生类型中的所有成员变量。在C#中,这意味着要使用反射。如本书条目43所述,反射有许多缺点,特别是当程序性能非常重要的时候。而等同性判断又是一个在程序中被频繁调用的基础性功能,所以在这里追求性能是个很合理的做法。因此,几乎在所有的情况下,我们都应该为自己的值类型提供一个更快速的Equals()覆写版本。对值类型Equals()实例方法的建议非常简单,即无论何时创建一个值类型,都要覆写其ValueType.Equals()方法。
不过对于引用类型,只有当我们希望更改其预定义的语义时,才应该覆写Equals()实例方法。.NET Framework类库中的许多类都使用值语义而不是引用语义来做等同性判断。例如,如果两个string对象包含相同的内容,它们将被认为相等。若两个DataRowView对象引用同一DataRow,那么将被认为相等。这样,如果某个类型需要遵循值语义(比较内容),而不是引用语义(比较对象标识)的话,我们就应该覆写Object.Eq
0人参与评分
很好(5星,4星):
一般(3星,2星):
不推荐(1星):&&&
写购物评价,赢购物积分,可换购物券!好书不要私藏哦,分享给别人吧!
温馨提示:由于每位咨询者提问时间及蔚蓝网促销等不同原因,以下回复仅对提问者3天内有效,其他网友仅供参考!
1件商品成功放入购物车
购物车共件商品,商品金额合计¥97.10
购物车中已有该商品
非常报歉,本商品没有库存暂时无法购买,如果您仍需要该图书
请输入邮箱做缺货登记,到货后我们会给您发邮件通知。
非常抱歉,您购买的数量超过库存,当前最多可买303件,请您修改。

我要回帖

更多关于 微信免费送手机付运费 的文章

 

随机推荐