C#代码--编写高质量C#程序

C#代码--编写高质量C#程序

ID:83010962

大小:222.51 KB

页数:111页

时间:2022-11-14

上传者:无敌小子
C#代码--编写高质量C#程序_第1页
C#代码--编写高质量C#程序_第2页
C#代码--编写高质量C#程序_第3页
C#代码--编写高质量C#程序_第4页
C#代码--编写高质量C#程序_第5页
C#代码--编写高质量C#程序_第6页
C#代码--编写高质量C#程序_第7页
C#代码--编写高质量C#程序_第8页
C#代码--编写高质量C#程序_第9页
C#代码--编写高质量C#程序_第10页
资源描述:

《C#代码--编写高质量C#程序》由会员上传分享,免费在线阅读,更多相关内容在行业资料-天天文库

1.1换行的讲究51.1.1寻找最佳的断行位・51.1.2每行只写一条语句?1.1.3分行定义变量81.2避免代码过于拥挤91.2.1使用空行分隔代码块!01.2.2使用空格降低代码密度131.3如何缩进151.3.13.1嵌套或包含关系引起的缩进!71.3.2因换行而产生的缩进201.3.3使用空格还是Tab键211.4大括号221.4.1大括号的位*221.4.2空的大括号结构231.4.3仅包含单个语句的结构体261.5保持项目文件的条理性291.5.1解决方案的结构呼应291.5.2代码文件的结构291.5.3使用#region标记32第2章养成良好的注释习惯331.1何时需要注释332.1.I解释代码的意图343.1.2对局部变量的说明354.1.3充当代码标题365.1.4指出例外情况406.1.5开发提示407.2注释的格式412.2.1单行注释422.2.2多行注释44

12.3正确使用XML文档注释45

22.3.1结构与类的XML文档注释462.3.2属性的XML文档注释472.3.3方法的XML文档注释492.3.4构造函数的XML文档注释502.3.5事件的XML文档注释512.3.6枚举类型的XML文档注释532.3.7泛型的XMし文档注释532.3.8其他标记54总的来说,如果多看看MSDN自身的类库参考,你会发现其实XML文档注释最终形成的就是这样的结果。MSDN本身就是ー个最好的XML文档注释的范例,我们可以在实践过程中不断地进行模仿和学习。第7章如何使用函数577.1为什么要使用函数577.1.1函数与方法577.1.2代码复用597.1.3隐藏细节617.2函数重载651.2.1重载的语义657.2.2保持核心代码唯一707.3参数的设计?47.3.1参数的命名747.3.2不要使用保留项748.3.3变长参数列表759.3.4何时使用ref参数和ou1参数7710.3.5参数的顺序7811..6重载函数的参数一致797.4参数检查827.4.1检查零值及空引用837.4.2检査枚举类型847.4.3防止数据被篡改857.4.44.4在何处检査合法性87

37.5.1返回值的用法887.5.2离开函数的时机89

4假设我们写的是文章而不是程序,那么你ー定觉得诸如文章应该分为若干个自然段、每段开头空两格之类的规则是理所当然的。如果段落的开头不空两格,或者干脆把整个文章写成单独的一段,仔细想来似乎也不会影响文章实质内容的表达。既然如此,我们为什么还要在形式上下功夫呢?设想一下,如果你手中的这本书既无章节也无目录,正文中的不同内容都使用同样的字体字号印刷,几百页纸从头至尾洋洋洒洒如念经般地‘‘ー气呵成”,你还有耐心看下去吗?这是ー个人人都能理解的道理,可是当文章变成程序的时候,就不是每个人都能想得通的了。不仅仅是初学者,甚至一些熟练的开发人员,也会写出凌乱不堪的代码。许多人一定有过这样的经历:一年半载之后,自己原来写的程序就完全看不懂了。如果这段程序只是为了交作业,或者临时ー用,那还可以不去追究,但如果这是一个商业软件,现在需要根据客户的要求进行修改的话,工作量可就大了——你不得不先花时间把你原来的思路看懂。肯定会有人反驳:代码是给机器运行的,又不是给人看的,写那么好看有什么用?

5他的话只对了前半句:代码确实是给机器运行的,可是机器总共オ需要看它儿分钟?你花ー个月编写的程序,机器顶多两三分钟就编译好了——在这两三分钟之前,这代码不都是你在看吗?开发软件编写代码不是一朝一タ的事情,更多的情况下,ー个软件的开发要经历很长的时间,并且常常由多人合作完成。一个庞大的软件项目,可能会动用上千名程序员工作数年!如果把代码写得连自己都看不明白,怎么与别人交流?同一个开发团队内,一定要保持良好且一致的代码风格,才能最大化地提高开发效率。有的初学者会问:我现在只是一个人写程序,并不需要和其他人合作,这些条条框框还有什么必要吗?要知道,团队协作只是ー个方面。我经常遇到这类情况,ー些初学者拿着他的程序来说:“这个怎么不能编译?”我帮他把代码整理了半天,发现有一个地方丢了半个大括号。如果他写程序的时候能够稍加注意一些的话,相信此类错误完全可以避免。保持良好的编程习惯,能够避免的错误还远不止这些。如果说程序代码中对算法的清晰表述是通过长期训练而获得的,那么本章要介绍的这些方法则无需伤神,你不必对代码做任何实质性的改动,只需要添加一些空行与空格,就可以使其可读性大大提高——这些规则就像写文章应该分段ー样简单,只要愿意遵守,那么别人在第一眼看你的代码时,必能感觉到你那良好的编程修养,即所谓“见字如见大”。1.1换行的讲究虽然你完全可以在C#里将所有的代码都连在一行里书写,但想必没有人愿意这么做,谁也不会自己折磨臼己的眼睛,何况大多数鼠标对于上下翻页的支持都比左右翻滚好得多。我相信,这也是大多数人接受将每条语句分行书写的原因,很少有人会怀疑这ー点的合理性。例如下面这行代码,虽然结构很简单,但是它实在太长了,所以被分成了两行:代码示例1T:由于代码过长而进行断行bitmap=newBitmap(size.Width,size.Height,System.Drawing.Imaging.PixelFormat.Format32bppArgb);这一点我相信大家都能理解并愿意遵循,然而问题的焦点并不在于要不要换行,而在于在什么位置换行。

6写程序不能像写文章那样,什么时候顶到了边界就换,而必须按照其语法规则,在可以换行的位置断开。例如,对于包含ー个超长表达式的语句来说,我们可以在某两个表达式项之间将其断开,如下所示:代码示例1-2:通过断行使代码更加清晰if(f==ImageFormat.Jpeg.Guid|f==ImageFormat.Tiff.Guid|If==ImageFormat.Png.Guidf==ImageFormat.Exif.Guid){supportsPropertyIterns=true;}else(supportsPropertylterns=false;)原本一个很长的条件表达式,通过在“|ド运算符处换行,显得更加地清晰。有一点需要我们注意的是,当我们进行折行时,要将折行位置处的分隔符(如前一例中的逗号,这一例中的"丨运算符等)留在上一行的行末,给人以“此行并未结束”的直观印象。这就好像在英文书写中,如果你需要将一个单词拆开,就需要在前一行末尾加上连字符,以表示那个单词并没有结束。可以看出,换行在防止代码超出屏幕边界的同时,还影响着代码的表达。因此如何选择合适的换行位置也是很有讲究的。有的时候,我们并不一定非要在临近右边界的时候オ去换行,如果存在更为合理的分法,就应当采用,例如下面的情况:doublecontainerAspectRatio=(double)container.Clientwidth/container.ClientHeight;

7按理说这样的断行并没有什么问题,它在表达式的两项之间断开,并将运算符留在了上一行的行末。但是,我相信如果换ー种断行方式的话,能够更加清楚地表达出原来的逻辑:

8doublecontainerAspectRatio=(double)container.ClientWidth/container.ClientHeight;如此一来,这个除法算术表达式就显得较为完整,相比前ー种写法而言更能体现其内在的逻辑关系。通常我们会选择整个表达式中最高的关系层次进行断行,例如上述代码中的“赋值号”和“除号”都是可以考虑的断行点,但相比较而言,除号连接的这个算术表达式只是整个赋值表达式的右半部分,如果在除号处断行,那么不但整个表达式会被截断,连局部的这个除法表达式也会被截断;反之,我们选择在赋值号处换行,可以保持除法表达式的完整,最大限度地减少换行对语句整体结构的破坏。同样的道理,为了将逻辑体现得更为清晰,我们甚至可以将函数调用中的每一个参数都分行书写,如同下面这样:代码示例『4:将函数调用中的每一个参数都分行书写RectangleimageBounds=newRectangle(itemBounds.X+padding,itemBounds.Y+padding,itemBounds.Width-padding*2,itemBounds.Height-padding*2);当参数数量较多,参数较长或者包含表达式的时候,这种排版比起单独写成一行更为直观醒目。对于LINQ查询表达式来说,将每个子句单独写成一行也是好的习惯。因为这同时符合了T-SQL语言的文化传统。例如:代码示例1-5:将LINQ查询表达式中的每个子句单独写成一行IEnumerablehighScoresQuery=fromscoreinscoreswherescore>80orderbyscoredescendingselectscore;

9如果说换行是为了防止屏幕左右滚动的话,那么当这个情况不存在的时候,•些人就开始打算充分利用屏幕空间了:privatestaticvoidSwap(objecta,objectb){objecttemp;temp=a;a=b;b=temp;}看起来好像确实没有占据多少屏幕空间,这只是把三条很短的语句凑在一行了而已ーー关键的理由是:它不会引起屏幕的左右滚动。但是当人们已经习惯于一行一条语句的时候,很可能就会忽视这里有三条语句这个事实(不要指望每次遇到的都像这个例子一样地简单)。更为重要的一点是,编译器总是按行来进行设计的,将多条语句写在一行会引起不必要的麻烦,例如:你将永远无法把断点设置在后面的语句上:AprivatesttlievoidSvtp(objectt,objectb)(objeCttfp;。37133•ib;bstep.)图!-i:一行代码包含多条语句时的断点设置しU有的读者会觉得,如果代码复杂,当然应该分开书写,没有必要去节省那点屏幕,但是如果像这个例子中这么简单,写在ー起也不会带来什么麻烦。单纯地看来,他的话不无道理,可是,对于一个开发团队,或者将要进入开发团队的人来说,最重要的是“统ー”。如果我们允许将多条语句合并到同一行代码内,那么怎样的代码オ算“简单”到可以合并的程度?是宽度小于50个字符的可以合并,还是宽度小于51个字符的可以合并?当一条规定无法被准确地定义的时候,它也就无法执行,从而必将在整个开发团队中产生不一致性,最终导致更多的混乱。1.1.3分行定义变量我们再来看ー种情况,这类代码出现的机率更为频繁,它是将相同数据类型的几个变量声明放在了同一•条语句中:intnum,factor,index,length;

10如果我说我反对这种写法,一定会有读者大叫起来:这明明是单独的一条语句,何况C#允许我们在一条语句内声明多个变量,如此一来还可以少写几个‘'int",为什么不行?这种写法,显而易见会给注释帯来很大的麻烦。把它们都写在ー起以后,我怎么给每个变量添加注释呢?如果是分开书写的,那么我可以很容易地为每ー个变量添加单独的注释,就像这样:代码示例1-6:将每个变量分行定义将有助于单独注释/Z要计算的数值intnum;//表示影响因子intfactor;/Z元素所在的索引号intindex;/Z数据列表的总长intlength;如果觉得这种写法较为繁琐,一定要节约那几个''int”,以强调它们的数据类型相同的话,也可以采取下面的写法:代码示例『7:变量分行定义的折衷方案num,/Z要计算的数值factor,//表示影响因子index,/Z元素所在的索引号length;/Z数据列表的总长这种方式只使用了一条声明语句,但是每个变量都书写在单独的行上,便于有针对性的注释。1.2避免代码过于拥挤想想人们为什么喜欢为文章添加各级标题以及其他复杂的格式,是因为美观吗?也许是的,但我相信这些格式可以更容易地让人们理清思路。可是在程序中,我们无法使用这些手段,所有的代码都是纯文本的,即使VisualStudio的代码高亮功能uj•以为代码的不同部分标上不同的颜色,但这并不能真正影响到代码本身。因此,光是换行还是不够的,我们还需要更多的手段。

111.2.1使用空行分隔代码块适当地添加空行则是一个非常有效的代码整理方式——有点像文章中的分段,一段意思也许需要若干个句子才能表达清楚,在表达下一段意思之前,应当另起一段。首先,每个C#代码文件是从命名空间引用开始的,ー组引用结束之后,则是命名空间的声明及类型的声明。很显然地,在命名空间引用与命名空间声明之间,应该留有一个空行以示区隔:代码示例!-8:在命名空间引用之后添加空行usingSystem;usingSystem.Collections.Generic;usingSystem.Drawing;usingSystem.Drawing.Imaging;usingSystem.10;usingSystem.Text;usingAvilla.Metadata;usingAvilla.Searching;/Z这里用空行隔开namespaceAvilla{//下面的内容省略ー个空行,意味着不同的功能块的分隔,如果读者稍加留心,就会发现VisualStudio自动生成的代码,总是在类型的各个成员之间留有一个空行。我们在书写代码的时候,也可以模仿这一格式:代码示例『9:在类型的各个成员之间添加空行IIIくsummary〉//Z表示一条搜索条件的抽象基类///〈/summary〉publicabstractclassSearchCondition

12Ill

II!初始化ー个くseecref="SearchCondition*/>类型的实例,并指明是否区分大小写IIIIII是否区分大小写く/param>protectedSearchCondition(boolcaseSensitive)(this.caseSensitive=caseSensitive;)/Z这里用空行隔开protectedboolcaseSensitive=false;IIIII!获取或设置ー个publicboolCaseSensitive{get{returncaseSensitive;}set{caseSensitive=value;})/Z这里用空行隔开IIIII!获取表示此搜索条件的SQL筛选条件表达式IIIIIIII!返回一个字符串形式的条件表达式,可直接用于SQL语言中的WHERE子句///

13publicabstractstringGetFilterExpressionO;

14这样排版无疑会使得每个成员的代码段更富独立性,绝大多数的编译器,在自动生成代码时都会遵照此方式排版。您可能会发现,上例中的caseSensitive字段与CaseSensitive属性之间并未留有空行,这是为了强调字段与其对应用于公开访问的属性之间的联系,关于类似情况,我们将在后面的章节详细讨论。然而,ー个空行意味着的不仅仅是功能模块的界限,它更是对代码逻辑块的划分。我们无法期望每个操作都只通过一行代码一条语句来完成,大多数情况下,它们都需要许多行代码来执行一个完整的操作。例如,你想查询数据库,那么你需要先生成SQL代码,建立命令,然后执行这个命令并填充至数据集。这中间大约需要使用四五行代码,而这四五行代码便组成了一个相对紧密的逻辑块,它与其后面的其他逻辑块即可以通过ー个空行来进行分隔。请看下面的一个例子:代码示例1T0:用空行分隔逻辑块publicstaticstring[]GetPhotoIds(stringfilterExpression,stringsort,boolcaseSensitive)(/Z第一个逻辑段代码根据处理后的参数取得数据行xml.Photos.CaseSensitive=caseSensitive;DataRow[]rows=xml.Photos.Select(filterExpression,sort??string.Empty);/Z遍历数据行,取出需要的元素string1]ids=newstring[rows.Length];for(inti=0;i

15这个函数的目的是根据指定的筛选条件和排序规则返回照片的标识号(PhotoIDs),函数内部自然形成了三个逻辑段:先是根据要求取得原始数据,然后从原始数据中提取我们需要的部分,最后将结果返回。用空行将这三个逻辑区分隔开来将会更加有利于我们理解其思路。关于注释的合理使用,我们会在后面的章节中再专门介绍。既然空行可以起到分隔代码,提高清晰度的作用,那么有的朋友也许会为了强调这种分隔效果,多加几个空行。可事实的效果是,连续的多个空行,在并未提高多少清晰度的同时,浪费了屏幕的空间,而且会让人觉得前后两个代码段并不相关——事实上它们应该是相继执行的。空行的意义和文章的段落ー样,仅在于表示一个停顿,而并非结束。1.2.2使用空格降低代码密度Basic、Pascal与C这三种早期高级程序设计语言的语法,至今仍在发挥着其重要的作用。VisualBasic仍然保留着Basic的句法简单,多用完整英文单词,贴近自然语序的习惯(如And、Not、!nherits,Implements,Handles等等关键字):而Delphi更是沿续着Pascal语言那标志性的BEGIN-END作风。C语言由于在操作系统开发上取得了成功,使得它在软件开发历史上占据了绝对的优势,相比而言,它的语法更加具有影响力,广泛被C++、Java、C#,乃至用于编写网页的ECMAScript/JavaScript和Flash的脚本语言ActionScript所吸纳,因此也变化丰富。但是它那种善用符号的古老特色一直被保留了下来,有理由相信,C语言是使用符号最多的语言。当其他语法体系都采用AND、OR等关键字作为运算符时,C语言却使用了“&&”、,ド’这样的符号,虽然在语法上并没有增加任何复杂性,但各种奇形怪状难以记忆的符号还是会令初学者望而却步。让我们来比较一下面的几行代码:BASIC:Ifa>bAndcOdOrNote>fThen...PASCAL:If(a>b)And(cOd)Or(Not(e>f))Then...C:if(a>b&&c!=d||!(e>f))...这三行的意义是完全相同的,但明显可以让人感觉到淸晰程度的差异,Basic和Pascal的代码看上去・很容易明白,而C语言的代码却像蚂蚁一般缩成一团。重要的原因在于:C语言的运算符几乎都只由’‘符号”构成,与变量名之间不需要用空格充当分隔符。这样ー来,由于缺少空格的稀释,C语言的代码就像被浓缩过似的——现如今它除了影响我们阅读以外,

16没有什么好处。因此我们有必要人为地添加一点空格,帮它降低代码的“密度”。这里,我总结了一些关于如何在运算符两侧添加空格的基本规则:1.单目运算符(UnaryOperators)与它的操作数之间应紧密相接,不需要空格。例如:代码示例1T1:单目运算符的空格规则示例y=++x;//++在这里是前缀单目运算,它与x之间无空格2.在双目、三目运算符(Binary/TernaryOperators)的左右两侧分别添加空格。例如:代码示例1T2:双目、三目运算符的空格规则示例inta=3+5;//在双目运算符左右添加空格intb=a*6+7;intc=a&b;intd=b++*c-:/Z虽然有单目运算符,但双目运算符两侧仍应添加空格inte=a>0?1:0;//在三目运算符左右添加空格3.括号(包括小括号、中括号与大括号)的内侧应该紧靠操作数或其他运算符,不需要添加额外的空格。例如;代码示例1T3:括号的空格规则示例intf=(a+b)*c;/Z括号内侧紧靠操作数,因其他运算符添加的空格留在外侧intg[MAX]={1,2,3);//中括号与表达式中的大括号也同样处理4.不要使用连续的两个或多个空格。其实,如果理解了这些规则,在实际书写的时候很容易遵循。对于任何・个表达式,我们先把单目运算符和括号去掉,然后在双目、三目运算符的左右两侧分别添加一个空格,再将单目运算符和括号填回去,放在靠近自己操作数的ー边即可。关于函数调用时,要不要在函数名和其后的括号之间添加空格的问题已经讨论了很久。其实这个是ー个无伤大雅的事情,无论使用何种方式,都不会对代码的可读性产生多少实质性的影响,纯粹是各人喜好罢了。不过在这里,我建议采用VisualStudio中的默认规则:在函数调用时不添加空格,而在ー些类似的带括号的语法结构中添加空格。请看下面这段代码:代码示例1T4:函数调用时的空格规则示例

17stringcmd=string.Empty;/Z函数形式的调用,括号前没有空格cmd=Console.ReadLineO;/Z语句结构,括号前有空格if(cmd.Length>0)(Console.WriteLine(cmd.ToUpper());)else{Console.WriteLine("(Empty)*);}这段代码中的ReadLine、WriteLine都是函数调用,因此与其后面的括号紧密相连,不需要添加空格。而if结构虽然同样带有类似的括号结构,但是它属于C#的内部语法,为了以示区别,在if与括号之间添加了一个空格。除if外,switch、for、while等都应做同样地处理。1.3如何缩进在有关代码风格的问题中,最为显眼的可以说就是代码的缩进(Indent)了。所谓缩进,是通过在每一行的代码左端空出一部分长度,更加淸晰地从外观上体现出程序的层次结构。为了更好地描述这一点,先请读者欣赏下列这段代码:intkmpmatch(char[]t,char[]p,int[]flink,intn,intm)(inti=0,j=0;while(i

18j=flink[j];if(j==m-1)(returni-m+1;}i++;j++;)return-1;}我想,就算让你检查ー下它里面有没有大括号配对错误恐怕都很困难,更不用说这段代码有什么功能了——你能一眼看清楚每个while循环的内容是什么吗?让我们换个方式,看看另一段程序:代码示例1T5:正确缩进的例子intkmp_match(char[]t,chart]p,int[]flink,intn,intm){inti=0,j=0;while(i

19if(j=m-1)

20returni-m+1;i++;jf)return-1;}两段程序,除了缩进的区别以外,一字不差。孰是孰非,相信大家都能看得出来,缩进的必要性不难理解。接下来的问题就是:应该如何缩进。1.3.1嵌套或包含关系引起的缩进当遇到有关命名空间、类、结构、函数、以及枚举等等复杂程序结构的定义的时候,我们通常需要将它的内容缩进ー层。在C#语言中,大括号是一个非常明显的标志,凡是遇到大括号,都应该直接联想到缩进。请看下面的示例:代码示例1T6:包含关系下的缩进namespaceMyNamespace(/Z命名空间内的内容应缩进publicclassMyClass{/Z类的成员应缩进publicstringMyMethodO(//方法函数的函数体应缩进return"Hello!;}

21privateMyEnummyProperty=MyEnum.Alpha;publicMyEnumMyProperty

22//属性的内容应缩进get(//属性的get部分函数体应缩进returnmyProperty;)set{//属性的set部分函数体应缩进myProperty=value;publicenumMyEnum{/Z枚举类型内容应缩进Alpha,Beta,Gamma)}分支结构(包括if…else结构、switch结构等)和循环结构(包括for结构、while/d〇…while结构等)都是存在嵌套关系的,因此从条理清晰的角度来说,它同样应该进行缩进ザ写:代码示例1T7:嵌套关系下的缩进//if...else结构

23if(a>b)//if子句的结构体内容应缩进max-a;min=b;}else{//else子句的结构体内容应缩进max-b;min=a;}//switch结构switch(n){//switch结构的内容应缩进case0://case子句内容也应缩进//...break;case1://...break;default://...break;

24//for结构for(inti=0;i<100;i++)(//for的循环体应缩进s+=data[i];t*=data[i];}//while结构i=0;while(data[i]!=0)(//while的循环体应缩进s+=data[i];t*=data[i];i++;}缩进时,应将内部结构中的所有语句都统一向右退一格,大括号则与原来的语句保持在同一个垂直位置上。每层缩进的长度应该一致,通常为ー个制表符宽或四个空格。还有一些细节的地方也与换行相关,例如if、switch、for这类具有嵌套结构的语句,在书写的时候都应避免将结构体与语句本身写在同一行上,关于嵌套结构的书写方法,我们会在后面的章节中详细讨论。1.3.2因换行而产生的缩进我们在前面提到过,当一条语句太长而超出一定的宽度时,应该折行书写。此时,从第二行起到该语句结束之间的各行应该缩进ー层,至下一条语句时再恢复原来的缩进位置。代码示例1T8:因换行而产生的缩进intmyVar=0;

25/Z这是一条很长的语句,因而出现了换行,从第二行起都缩进了一格:myVar=myVari+myVar2+myVar3-myVar4-myVar5*myVar6*myVar7/myVar8/myVar9+myVarlO+myVar11-myVar12-myVar13*myVarl4*myVarl5/myVar16;//后面的语句恢复正常的缩进位置Console.Write(myVar);如果该语句是进行函数调用,由于参数太多而造成的换行,那么在缩进规则上有一些微小的差别:代码示例1T9:函数调用时分行书写参数而引起的缩进RectangleimageBounds=newRectangle(itemBounds.X+padding,itemBounds.Y+padding,itemBounds.Width-padding*2,itemBounds.Width-padding*2);注意最后一行的括号与分号并没有缩进,因为这种结构其实是对类似if的大括号嵌套结构的模拟。1.3.2使用空格还是Tab键如何缩进一向是ー个有争议的问题。使用Tab及Shift+Tab键缩进在操作上非常方便,而使用空格可以保证代码在任何编辑器下都能正确显示缩进格式。现在,我们依靠VisualStudio开发环境则可以轻松解决这个矛盾:在“选项”对话框中对C#编辑器的代码缩进方式进行设置(如图!-2),选择“插入空格”模式。

26图!-2:VisualStudio中关于代码缩进的设置⑵.这样一来,我们就可以在书写代码时使用Tab键和Shift+Tab键来调整缩进,而VisualStudio会将其转换为空格保存至代码文件中。1.4大括号从外观上来看,类C语言的最大标志就是它无处不在的大括号了。在C#中,大括号仍然扮演着几种不同的角色:表示层次关系(如定义命名空间、类时使用的大括号)、表示复合语句(如if、for中的大括号)、表示数组元素。本节讨论了有关大括号书写的几个基本问题。1.4.!大括号的位置代码风格中,如何摆放大括号一宜是大们热衷的话题。其实,我更愿意用开放的态度去看待它,具体使用何种方式并不重要,重要的是,要保持方式风格的统ー,不能在同一个项目中出现不同的风格。最早出现的,也较为传统的是K&R风格。所谓K&R即指《TheCProgrammingLanguage》一书的作者Kernighan和Ritchie二人,这是世界上第一本介绍C语言的书,而K&R风格即指他们在该书中书写代码所使用的风格。K&R风格在处理大括号时,使用了・种较为紧凑的格式,将左括号留在前一行的末尾,并尽可能地压缩:代码示例120:K&R大括号位置风格publicintMax(intx,inty){

27returnx;}else{returny;)}据说,微软公司内部使用的就是这种K&R风格,然而它在对外公开的文档中,却使用更为大家所熟知的ー种风格,它将大括号单独写成一行。本书所有示例代码采用的都是这样•种格式:代码示例『21:C#默认使用的大括号位置风格publicintMax(intx,inty)(if(x>y)(returnx;)else{returny;)}虽然生硬地规定应该使用哪・种是不提倡的,而且也没有必要,但是我们仍然建议读者尽量选择上述两种风格中的一种,并在自己的程序中保持风格的统ー。1.4.2空的大括号结构所谓复合体,即指用于充当某个语法结构成份的,被大括号括起来的多条语句。C#的很多语法结构中都可以见到复合体的使用,如命名空间、类、结构、接口、枚举、方法、属性等的定义,以及if、switch,for、while-d〇…while、try…catch…finally结构等等。对于应该如何处理大括号的摆放以及内容的缩进排版等问题,我们都已经详细讨论过了,现在

28要考虑的是复合体自身的ー种特殊情况:空的复合体。

29接下来的问题是:为什么会出现空的复合体?有的时候来自于废弃的空函数。开发过程中,很可能只写了某个类或者函数的空声明,打算待日后再细化。然而由于设计上的变动,这个函数不再使用,而躲在代码某个角落的这个空函数又未能被及时删除,结果一直保留到产品发布。在这种情况下,建议在空复合体中添加“TODO”的注释:代码示例1-22:为未实现的空函数体中添加TODO注释publicvoidUnusedMethod()(//TODO:未实现的方法)因开发时的疏忽造成的空复合体还不仅仅是上面这ー种情况,我曾见到过有人将if结构中的if段留空,却在else里写上一堆代码,类似下面这样:if(table.Rows.Count—0)(}else(foreach(DataRowrowintable.Rows)(Console.WriteLine(row["Name"]);}}也许他本来在if中是有代码的,后来程序不断地修改,结果在if段中无事可做了。这种情况也很容易避免,只需很简单地将if中的条件表达式反转过来即可:

30代码示例123:反转if条件表达式以避免空的if子句if(table.Rows.Count>0)foreach(DataRowrowintable.Rows)

31Console.WriteLine(row["Name"]);如果不是开发时的疏忽,那么空复合体的出现就是有意而为之了。在循环结构中,它出现的频率相对较高。早期,人们会采用空循环的方法来达到“延时”的效果,那个时候常常会看到类似这样的代码:printf("Waiting...

32");for(i=0;i<10000;i++){)printf("Done.");或者干脆连大括号结构都没有,直接就是printf("Waiting...

33");for(i=0;i<10000;i++);printf("Done.");且不说现如今这种“延时”方式完全不可取,就从代码的外观来看,它也极易让人将for下面的那一行printf输出代码当成是循环的内容。由于C#语言继承了C语言语法灵活的特点,因此循环语句本身就能承担很多复杂的工作,以至于根本就不需要循环体。下面的代码反映了一种典型的状况:intsum=0;for(inti=0;i

34代码示例1-24:让for循环语句本身仅控制循环,不要涉及具体事务intsum=0;

35for(inti=0;i(internalPhotoCollection()(/Z空构造函数)/Z其他类成员已省略)其他情况下,如果确实有必要使用空的赁合体,也应当仿照上例书写,添加相应的注释说明,以防止产生误解。绝不能省略大括号结构,仅以ー个分号代替。1.4.2仅包含单个语句的结构体我们再来讨论另ー种特殊的复合体。按照C#的语法,如果if、while、for等等的结构体仅为一条语句,那么大括号是uj"以不写的——其实复合体就是将多条语句合并起来充当单条语句来用的。甚至我们可以将结构体与if、while,for写成一行,例如:if(a>b)x++;

36elsey++;或者是:for(inti=0;i<10;i++)dest[i]=source[i];虽然这在语法上没有什么问题,但当代码数量增加,上下文的代码变得熨杂时,我们有可能会对if、while,for的结构体究竟包含哪些语句产生误解。因此,我们建议,即使if、while、for结构的内容只有一条语句,也应该像处理多条语句那样,写在大括号内。因此,刚オ那两段代码就应该写成下面这样:代码示例1-26:即使结构体内只有一条语句,也必须写在大括号内if(a>b)(x++;}else(y++;}for(inti=0;i<10;i++)(dest[i]=source[i];}有的读者可能看不下去了:明明很简单的两三行代码,为什么要搞得这么麻烦?首先,要肯定的是,这并不会影响代码的实质。添加大括号结构是为了更加清晰地体现层次关系,使得任何人在扫一眼代码之后,不会对此处的逻辑结构产生误解。在实际的开发中,我们遇到的

37问题远比上面给出的要复杂,例如:代码示例1-27:ー个较为复杂的if嵌套privatestaticboolIsValidDate(intyear,intmonth,intday)

38if(day<1||month<1||month>12)returnfalse;)if(month=2)(if(year%4=0&&(year%400==0|year%100!=0))(returnday〈=29;)else(returnday〈=28;))elseif(month=4month==6|month==9|month==11)(returnday<-30;)else(returnday<=31;

39)}这是ー个判断给定的日期是否合法的函数,它包含了多个条件判断的嵌套,如果不使用大括号,那么阅读起来,产生误解的机率就会大大增加。因此,即使if、for、while语句的结构体中只包含一条语句,也应该置于大括号内。

401.5保持项目文件的条理性如果说我们之前讨论的代码风格问题仅仅局限于单个代码文件中的一个局部,那么在本节中,我将从更高的层次来展示这种条理性。包括解决方案的结构呼应,代码文件的结构,以及#region标记的使用。1.5.1解决方案的结构呼应C#程序的最高层次是“解决方案(Solution)",每个解决方案包含ー个或多个“项目(Project)"〇这些项目也许是Windows窗体应用程序,也许是DLL程序集,或者是ー个Web站点。项目由于其自身的特点,在划分时通常不会存在什么争议図。这里我们主要讨论项目内部的组织结构。当我们在项目中建立文件夹时,Ct会自动将文件夹作为子命名空间的名称。例如,如果原项目的默认命名空间是MyProject,我们新建了一个名为SubFolder的新文件夹,那么这个文件夹下的新建代码文件就会自动以MyProject.SubFolder作为默认的命名空间。我们建议保持这样的ー个呼应规则,通过文件夹直接反映命名空间的层次结构,而不要将文件夹作为附加的给代码文件进行分类的手段,比如将同一个命名空间下的代码文件归入“类”、“接ロ”、“结构”等子文件夹中去,这不仅给开发的过程带来麻烦,也破坏了命名空间名称与文件夹结构之间的呼应关系。对于每ー个代码文件(指后缀名为cs的文件)来说,我们通常会为每个类单独建立文件,但当人们面对ー些尺寸较小的类型,尤其是枚举时,就会将其与需要它们的类型定义在同一个文件里。例如Logger类也许需要为Logged事件提供LoggedEventArgs类型的数据,而这个LoggedEventArgs类经常会被人图方便与Logger类定义在同一个文件里。我要强调的是,虽然C#允许在同一个代码文件里定义多个类,但为了使得工程结构更为清晰,请不要那样做。我们应当恪守一个原则:ー个代码文件中只定义ー个类型。这里的类型包括类、结构、接口、委托或枚举。而且,这个代码文件的文件名应当与类型的名称完全相同。例如Logger和LoggedEventArgs两个类密切相关,甚至除了Logger类以外,不会再有第二个类型会直接使用到LoggedEventArgs类型,但是即便如此,我们仍应将其分开写在不同的代码文件中,分别命名为Logger,cs和LoggedEventArgs.cs。

41如果一个代码文件里声明的是一个枚举,那估计它的内容也不会太长,出不了什么乱子。如果是ー个有着几十个成员的类,事实恐怕就要复杂得多了。有没有过在茫茫代码中看花了眼,怎么也找不到需要的函数的经历?我曾经花了半分多钟オ从一位朋友的代码中找到某个类中实际进行实例初始化的静态方法。改变类中各成员的前后顺序并不会对程序的运行产生任何影响,但如果能坚持以特定的顺序排列成员的话,确实会大大提高我们阅读代码的效率。至于如何排列,并没有唯一的标准,我下面给出的仅是ー种参考:1.总体上按照public、internal、protected,private的访问控制顺序排列;2.总体上按照构造函数、Finalize函数、字段、属性、方法、事件、委托的类型顺序排列;3.尽可能将相关性强的成员排列在ー起。上述三条表述仅仅是一种确定排列顺序的大体方式,并不可能完全遵守,具体情况则要复杂得多。出现在最前面的应该是构造函数,这是由构造函数自身的特殊地位决定的。在使用ー个类的时候,首先接触到的就是构造函数,因此我们有必要使构造函数出现在最为显眼的位置。如果有多个构造函数,则按照public、internal、protectedxprivate的顺序放置。绝不要将private构造函数放在其他成员的后面,因为对于类来说,“没有构造函数”与“没有公开的构造函数”是两个完全不的概念。如果这个类没有定义任何构造函数,那么事实上编译器会其添加一个默认的公开构造函数;相反,虽然没有公开的构造函数,却有一个private构造函数,那么这个类将不再具有默认的公开构造函数,成了一个不可从外部实例化的类。因此,即使构造函数是私有的,也要写在类的最前面。接下来是Finalize函数,这是承袭了C++中的习惯。Finalize函数的情形非常简单,要么没有,要么只有一个。构造函数与Finalize函数之后的,是所有可被公开访问的成员!J!。首先是符号常量字段。这个习惯来自于C/C++中#define位于最前的规定,这也与其他语言中对于符号常量定义的习惯相一致。其后是属性,具体的顺序以体现逻辑相关性为佳,当然也可以按照字母顺序排列。例如,同样是表示电子邮件的MaiIMessage类的几个属性,就可以采用下列两种排列方式:

42From(发件人)AttachmentsTo(收件人)BccCC(抄送)BodyBcc(密件抄送)CCAttachments(附件)FromSubject(主题)SubjectBody(正文)To左边是以逻辑顺序排列的,右侧是以字母顺序排列的。当遇到具体情况时,可以根据其自身特点选择合适的排列顺序,在这个电子邮件的例子中,以逻辑顺序排列就要比字母顺序好得多。此外,很多属性的值是保存在相对应的私有字段中的,对于这种情况,我们习惯上将该私有字段与属性访问器写在ー起,类似于下面这样:代码示例1-28:私有字段与属性访问器的相对位置privateintage=0;publicintAge(get{returnage;)set(if(value<0)(thrownewArgumentOutOfRangeException("Age","Agemustbenotlessthanzero.");

43age=value;

44这样做可以让人一眼看出age字段与Age属性的关系,它们两者之间的相关性极强。这也可以避免阅读到Age属性代码的时候,再到处寻找age的定义。方法通常是一个类的主体,因为它不仅用于向外界公开操作,还为内部提供了丰富的可反复调用的函数。排列在属性之后的即是公开的方法成员。通常静态方法在前,实例方法在后,其中各方法的相互顺序与属性情况类似,既可以根据逻辑关系排列,也可以根据字母顺序排列。对于方法的多个重载(Overload)来说,应当将同一个方法的公开的重载连续写在ー起,并根据参数的数量由简至繁排列。例如File类的Open方法具有三个公开的重载:FileStreamOpen(stringpath,FileModemode)FileStreamOpen(stringpath,FileModemode,FileAccessaccess)FileStreamOpen(stringpath,FileModemode,FileAccessaccess,FileShareshare)由于通常情况下,简单的方法重载都是对复杂重载的调用,因此这样的顺序较好地符合阅读者的思维走向。公开方法之后依次排列的应为事件、委托、对接U的显式实现等,它们的内部顺序与前面所述的大同小异,这里不再赘述。直至给出所有公开成员之后,オ是受保护成员与私有成员,其内部顺序与公开成员相似。1.5.3使用#region标记在C#中,有一个标记与预处理命令非常相似,然而它却并不会对程序的编译与运行产生任何影响,仅仅在外观上起到隐藏细节的作用,这就是"#region/#endregion”标记。之前,我们提到受保护成员和私有成员应排列在所有公开成员之后。我们完全可以通过将受保护成员和私有成员分别放入#region/#endregion标记中,来隐藏受保护和私有的成员,仅展示公开的成员。密切相关的ー组成员也可以通过这种方式来突显它们的整体性,使得代码结构更加清晰。对接口的显式实现便是ー个绝好的例子。用于显式实现接口的成员很少会被直接调用,必须经过强制接口类型转换,那么将一个接口的所有显式成员包含在ー起,既不会干扰视线,也更富条理性。阅读代码的人会知道这个类已经实现了这个接口,至于这个接口有什么用,包含什么成员,那是另外一回事了,可以通过查看那个接口的定义来了解。

45诸如窗体类型及其他UI控件类型这样的代码文件屮总会充斥着大量的事件处理过程,而它们本身往往只是对其他函数方法的简单调用,并没有什么实际的内容。如果将所有事件处理过程也用#region/#endregion标记起来的话,可以减少冗长的事件处理函数过程,使得代码结构更加清晰。#region的用途还可以扩展到其他方面,如果有一组私有函数是集中对某个功能的实现,那么为了清晰起见,也可以将其归入ー个Region结构中等等。总之,我们的终极目的是仅直接暴露那些公开的成员,尽可能地隐藏细节,并使得代码看上去整洁,避免成员过多而显得臃肿。第2章养成良好的注释习惯注释(Comments)在程序中的作用极其微妙,它既是最不重要的,也是最重要的。说它最不重要,是因为将任何程序代码中的所有注释统统删掉,丝毫不会影响软件的运行,或者说,注释根本不会被编译输出到最终的软件产品中去。而说它最重要,是因为注释是程序代码中唯一采用自然语言书写的部分,它对代码是否能被阅读者正确而快速地理解起着决定性的作用。正因为如此,这种完全不会影响程序运行的’‘代码”,往往在程序中占据了最大的比重。不同的人在编写程序是总会有不同的思路,因此在阅读别人写的代码时,常常存在理解上的障碍。何况时间久了,有时连自己的代码都不一定能看得懂。为代码添加注释可以提高效率,提高代码的可读性,便于团队合作,同时也可以增强代码的逻辑性。在C#中,注释还被赋予了更为重要的任务。ー项名为“XML文档注释(XMLDocumentation)”的技术使得C#编译器可以将C#代码中的这些特殊注释提取编译为程序集的描述文档,它的内容信息可以用于编辑器的智能感知(IntelliSense™)系统,也可进ー步转换为我们需要的参考文档格式(例如MSDN的类库参考)。

46摆在我们面前的第一个问题即是“何时需要注释”,其实更本质的ー个问题是‘‘为什么需要注释”。在本章的引言中,我已经提到了一些,现在需要进行更深入的讨论。理解高级语言比起理解底层的机器语言来说已经容易得多,但直至今日,计算机语言与人类自然语言之间依然存在着不小的差距。从最基本的需要来说,注释是为了填补这两者之间的空白,起到ー个思维连接的作用。可能开发人员对计算机语言的熟悉程度不够,或者它在此处选用了相对较为陌生的技术或构件,注释可以对他自己或他人起到提示的作用,从而帮助理解和开发。如果我们假定开发人员对自己所用的语言和技术都是非常熟悉的话,那么注释还需要吗?事实告诉我们:需要。虽然在绝大多数情况下,开发人员都熟知自己所用的语言和技术,但注释仍然大量存在,可见对陌生内容的“注解”并非它的主要意义。事实上,使得注释存在的最为关键的原因是:人类进行程序设计与程序代码本身并不在同一个逻辑思维层面上。人们思维的时候是“任务式”的,而程序代码则必须是“过程式”的。人们在设计的时候,也许只是考虑“将这组数据由小到大排序”,而程序代码则需要数行甚至数十行才能完成,这种细节过程在某种程度上反而会影响人们对程序整体思路的理解,此时便需要注释来体现人们的设计意图。1.1.1解释代码的意图代码本身已经非常详尽地体现出它将如何工作,因此注释起到的应该是一种思维抽象的作用,通过符合人类思维方式的描述性文字从更高的思维层次阐释细节代码的功能与意图。从这个角度来说,我们很容易就可以看出,下面的注释是没有意义的:/Z定义ー个整型变量intn;这个注释并没有说错什么,但是它说了一句废话。这行短短的代码本身非常清晰地表达了“定义一个整型变量”的意思,它甚至比这个注释的内容更丰富——这个变量的名称是“n”。这条注释仅仅对那些看不懂程序语法的人来说オ有价值,

47而这种情况在真实的软件开发中是不存在的。相比而言,下面的注释就要有意义得多:代码示例2-1:有意义的注释/Z交换a、b变量的值temp=a;a=b;b=temp;三行代码仅仅是三个赋值操作,而这条注释给出了比三条个赋值操作更为抽象的意义,它反映出了这三个赋值操作的真正意图,因此这是一条有价值的注释。1.1.2对局部变量的说明如果变量的名称本身无法非常清晰地体现它的所有含义的话,就有必要通过注释来进行说明。对于成员变量来说,我们习惯上使用XML文档注释(我会在本章稍后的时候专门讨论),而普通注释则多用于说明局部变量。下面则是ー个很好的例子:代码示例2-2:对局部变量意义的注释/Z构成路径的Bezier曲线控制点集Point[]pathVectors=null;这条注释很全面地解释了变量pathVectors的作用,现在我们不但知道它是ー个Point结构数组,还知道了这个Point数组中存放的是构成一条路径的Bezier曲线控制点。相反地,如果变量名称所表达的信息已经非常完备(如currentDateTime),那么则可以不需要通过注释加以重复。很多情况下,数据的合理取值范围会小于(而且往往是远远小于)其数据类型的允许范围。对于颜色信息中的红色分量(当然也可以是绿色或蓝色分量)来说,我们也许可以猜到它的取值范围是0至255的整数,因为几乎所有的程序都会这么做。然而,如果换成透明度会怎么样?还是从〇至255的整数吗?有没有可能是从0至100的整数?或者是从0.0至U1.0的浮点数?〇究竟表示完全透明还是

48完全不透明?这些我们都无法确定,因为各种情况都在实际的程序中出现过。此时,在注释中进行解释是有必要的:代码示例2-3:对局部变量取值范围的注释/Z用于控制图像的透明度,〇表示完全透明,1表示完全不透明。doubleopacity=1.0;这样ー来就避免了因不一致而导致的难以觉察的错误。除了取值范围之外,数据所使用的数量单位也是至关重要的。图像的长度宽度数据并不一定总是以“像素”为单位,它也可能是“点"、“磅”、“厘米”或者“英寸”,当数据的单位可能会存在混淆或误解的时候,就有必要在注释中加以说明:代码示例2-4:对局部变量数量单位的注释/Z图像的尺寸,以英寸为单位SizeFimageSize=newSizeF(5,3.5);对于i、j之类的循环变量,由于其作用在所有的程序中都是显然易见的,可以不必添加说明。反之,如果与普通的循环变量相比存在着一定的特殊性,那么就应该考虑使用其他的循环变量名称,我们将在第6章“循环结构”(第107页)中详细讨论这ー点。2.1.3充当代码标题注释的另ー个常见的用途是以类似小标题的方式对代码进行提纲契领地描述。这与前面我们所说的“解释代码的意图”是一致的,不过我们要从另ー个角度来考虑它。单独考虑下面这一行注释,似乎它显得有些多余,因为它并没有能给出比代码本身更丰富的信息:/Z绘图像graphics.Drawlmage(attachedPhoto.Image,imageBounds);但是如果把它放在上下文中之后,我们就会发现,此处的注释很好地起到了概括的作用:代码示例2-5:起到概括作用的标题式注释

49/Z设置图像边框区域RectangleimageBorderBounds=newRectangle(imageBounds.X-borderWidth,imageBounds.Y-borderWidth,imageBounds.Width+borderWidth*2,imageBounds.Height+borderWidth*2);/Z设置文本区域RectangletitleBounds=newRectangle(itemBounds.X+padding,itemBounds.Y+itemBounds.Width,itemBounds.Width-padding*2,itemBounds.Height-itemBounds.Width-padding);/Z准备绘图graphics=Graphics.Fromlmage(bitmap);graphics.InterpolationMode=InterpolationMode.Bicubic;graphics.SmoothingMode=SmoothingMode.AntiAlias;graphics.TextRenderingHint=System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;/Z绘边框graphics.Fi1IRectangle(Brushes.White,imageBorderBounds);graphics.DrawRectangle(Pens.LightGray,imageBorderBounds);/Z绘图像

50graphics.Drawlmage(attachedPhoto.Image,imageBounds);/Z绘标题graphics.DrawString(title,SystemFonts.MessageBoxFont,titleBrush,titleBounds);graphics.Dispose();一眼看过去,我们可以很清楚地掌握这段代码的总体结构,一旦发现问题需要修改的时候,也可以很快地定位到可能出现错误的两三行代码处。对于分支与循环控制结构来说,标题式的注释也很常见,需要注意的一点是,注释的表述要符合控制结构本身的特点,并且要留意注释的位置,避免混乱。例如下面的这个if结构://当日志文件不存在时,提示错误信息if(File.Exists(logfile)==true)(MessageBox.Show("找不到指定的文件。”);)看上去似乎没有什么问题,但“提示找不到信息”是在这个if条件成立时オ会发生的动作,它属于if的结构体内容,与注释目前的位置并不相称,试想如果这个结构后还有else子句,那么是否也要将它们写在一起?//当日志文件不存在时,提示错误信息,否则将其读入if(File.Exists(logfile)==true)(MessageBox.Show("找不到指定的文件。”);)elseLoadFile(logfile);

51在这样短的if结构中,这也许还不至于造成麻烦。假如if结构体中的语句数量远远不止这ー两行代码,甚至整个if结构的长度超过你屏幕能显示的范围时,这种形式的注释就会产生一个基本的错误:离被注释的代码太远。因此,我们应当将注释的内容分开,采取更符合逻辑的方式,在判断还未发生之前,不要引入判断之后的事情:代码示例2-6:对if结构的注释方法/Z检查日志文件是否存在if(File.Exists(logfile)==true)E//如果不存在,则弹出错误提示MessageBox.Show("找不到指定的文件。”);)else(//如果存在,则将数据读入LoadFile(logfile);)类似地,switch结构开始处的注释应当仅仅对整个switch需要判断的内容进行说明,至于每种情况之后采取什么动作,则应留到各个case子句后再去注释。循环结构则与分支结构有所不同,由于事实上循环结构是对ー组对象分别进行相似的操作,因此位于循环语句之前的注释则应重点描述整个循环的迭代方式,而循环体内的注释则是描述如何处理每次迭代的对象。例如:代码示例2-7:for/foreach循环的注释方法/Z遍历所有选中的文件名称foreach(stringfilenameindig.Filenames)//以追加方式打开文件

52StreamWriterw=File.AppendText(filename);/Z写入当前的日期和时间w.WriteLine("Date:{0}",DateTime.Now.ToLongDateStringO);x.WriteLine("Time:{0}",DateTime.Now.ToLongTimeStringO);/Z关闭文件y.Close();)1.1.4指出例外情况有时为了达到某种特殊的效果,或者由于某种客观原因,我们需要在代码中使用ー些非常规的手段,甚至别人看起来会误认为这是一段错误的代码。如果某个“好心人”动手帮你改掉了这个“笔误”的话,那可真是帮了倒忙了。为了避免这种情况的发生,凡当我们使用了非常规的手段或技巧时,一定要加以注释,说明并不是我们大意疏忽,而是有意为之,不仅如此,还应当解释说明为什么要采用这种反常的做法。在前一章有关大括号结构的讨论中,我们就已经提到,当我们确实需要使用ー个空的结构体或函数体时,不能简单地空置,而需要在这个空的大括号结构内添加注释,表示这个空结构并不是笔误或者未完成的代码,而是它确该如此。1.1.5开发提示任何一个程序的开发总是需要经历ー个过程的,而代码也是由简单粗糙逐步走向完善健全的。起初,也许我们只是实现了某个函数的基本功能,但是并没有完善它,还缺少许多诸如数据验证、异常处理之类的功能。此时,为这段代码加上ー个注释是不错的选择。VisualStudio本身支持一些特殊的注释文本,称为“任务列表”。当我们在注释中使用这些特定文本的时候,编译器可以将它们收集起来。这其中最为我们所

53熟知的就是“TODO”注释,它通常可表示一切类似“还需要进ー步的工作”的含义。例如下面这个函数:代码示例2-8:TODO注释protectedQuestion(Questionnaireowner,XmlElementxmlElement):base(owner)I//TODO:验证xmlElement的结构xml=xmlElement;)我们可以通过“任务列表”窗格(从“视图”菜单中选择“任务列表”项)查看到所有的任务列表清单,如下图所示:TODO;KSQutftfofUf49图2-1:VisualStudio中的“任务列表”窗格[51除了“TOD〇”之外,还可以使用“HACK”和“UNDONE”标记。事实上,我们还可以自己创建这类特殊的注释文本。然而,从清晰的角度来说,使用“TODO”已经足够了。2.2注释的格式C#沿用了与C/C++完全相同的注释语法,“〃”可以注释至行末,“/小••*/”可以注释任意位置的一段文字。虽说这只是简简单单的两个符号,但经过一代又一代程序员的奇思妙想,注释的形式层出不穷,到处可以见到排版精美,镶嵌着漂亮花边的注释,比如:/・这是ー个排版精美的注释・/这种注释非常常见,它也有许多变种,下面的这ー个来源于MINIX操作系统的源代码倒:

54这是上一个注释的变种*/其他各式各样的形式相信你也有所目睹,然而ー个基本的问题随之摆在我们面前:注释需要如此精美的外观吗?别忘了我在上一节里所进行的讨论,注释的重要意义在于使代码的思路更为清晰,而不是给程序添加一些装饰。很多人热忠于这些复杂的注糅格式,也许是为了达到强调的目的,使得某些注释比其他注释更为“醒FI”——这无可非议,上面那个从MINIX操作系统源代码中摘出的注释就是起到这个作用:它在每个函数的开始处使用这种重量级的注释来加强函数的名称,而在其他地方只使用了常规的注释格式。我并不反对使用某种方式来对注释进行强调,但我反对在调整精美的注释格式上花费过多的时间。上面提到的这些注释格式都需要程序员亲手ー个字符一个字符地输入,手动对齐左右的星号,将文字居中,如果注称内容很长的话,工作量更大。假如日后对程序进行了修改,从而需要修改注释的时候——这经常会发生——如果你删去了前面一行中的ー两个字,那么后面每一行都需要调整:要么你选择让前一行缺两个字,给你精美的注释留下一处遗憾;要么你就得花费许多Del键和空格键的操作来重新建立这个漂亮的方框。这个问题发展到极端就会出现一个可怕的结果:为了保持注释的美观,干脆不修改注释。这不是完完全全本末倒置了吗?事实上,由于C#中支持使用XML文档注释来为关键的类型和成员添加说明,普通注释基本仅限于在函数的内部使用。这样ー来,大动干戈去搭建复杂的注释样式就显得没有必要了,常规的不加任何修饰的注释格式完全可以满足我们的需要。如果你仍然觉得不够清晰,可以使用#region来整理,不过此时往往意味着你的函数写得过于复杂了,应该考虑分解为更多的函数。2.2.1单行注释单行注释是最为常用也是最灵活方便的,可以自成一行,也可以置于行末。自成一行是较为普遍的做法,它位于要注释的代码之前,与要注释的代码块在缩进上保持一致,之前最好留有一个空行。例如下面的这一段代码:代码示例2-9:单行注释样例protectedvoidbtnAddNewClick(objectsender,EventArgse)

55//定位控件TextBoxtxtChoiceText=tableChoices.FindControl("txtChoiceText")asTextBox;/Z创建新选项数据对象Choiceitemnewitem=newChoiceItem(owner);newitem.Text=txtChoiceText.Text;//添加数据Additem(newitem);/Z显示ShowListltem(newitem);txtChoiceText.Text=}在书写时要注意,“〃”之后与注释文本之间应留有一个空格,避免拥挤。下面的一些格式已经不再适用,应避免使用://居中的注释文本以及,//=================带有イヅ号装饰的注释文本==================这种注释形式需要手工进行居中排版,当需要修改时显得很不方便。也不要使用,ソ・…*/”来标记单行注释,这会显得非常古怪且没有必要。对于ー组并列的注释来说,如果屏幕空间允许,那么放置在行末也是不错的选择。下面给出的是使用行末注释的典型情形:代码示例2-10:行末注释样例//包含各状态的缓存图//正常状态privateBitmappaintNormal=null;

56privateBitmappaintHovering=null;/Z悬停状态privateBitmappaintSelected=null;/Z选中状态privateBitmappaintHoveringSelected=null;/Z选中并悬停状态它唯一的缺点是,一旦代码部分被修改,可能会破坏注释部分原有的对齐,产生额外的代码格式化工作量。2.2.2多行注释多行注释主要用于代码文件开头处的版权说明部分,或者是在函数体内的篇幅较长的注释文本。为了突出它的注释性质,我们建议多行注释采用下列格式:代码示例2T1:标准的多行注释/*・这是一段很长很长很长很长的注释文本,以至于我们无法单独地将它放置在ー个・由“〃”引导的单行注释中。多行注释应当使用这样的格式。VisualStudio・对这种格式提供了内置的支持。・/对于篇幅很长的注释,应当使用上述注释格式,确保每一行的开头都有一个星号,而不是简单地使用''/*…*/”进行标注。整个多行注释由单独的“/*”起始,顶格书写,其后不跟随任何文字。所有的注释文本从第二行开始,开头空一格,然后是一个星号,与第一行的星号对齐,再空一格之后书写注释文本。如此反复,直至注释文本全部写完。最后以单独的“*/”结尾,开头空一格,使所有的星号全部对齐。在本节开始处提到的这种格式已经不再适用,应当避免使用:/・这是ー个多行注释・//・它采用了星号围绕的方框格式・//・它已经不再适用・/如果你实在觉得不这么写不能强调那句注释之重要,那么作为备选方案,你可以考虑如下格式:代码示例2-12:带有简单装饰口.易于修改的多行注释样例

57这是一个被强调的注释相比前一种方案来说,它更容易修改虽然我并不提倡这种写法,但是比起前ー种用星号围绕的格式来说,这个修改起来要方便得多。不过,如果有朝一日,开发环境之强大足以使得我们能够只需要按一个快捷键就自动把那一圈星号排列整齐的话,使用那样漂亮的注释也未尝不可。2.3正确使用XML文档注释在过去,我们也会为每个函数、类添加注释,说明它们的功能和用法,而XMし文档注释(XMLDocumentation)不但可以做到这一点,而且可以做得更好它与VisualStudio本身的代码智能感知系统无缝集成了。当我们在使用.NETFramework系统类型的时候,代码编辑器会提示我们每个类的相关信息,如果使用了XML文档注释,也可以让我们自己编写的类实现这ー点。举ー个最常见的例子:代码示例2-13:XML文档注释示例///〈summary〉///表示一幅照片。II!〈/summary〉publicclassPhoto(〃成员省略}rOp«fM»of芻W8;0&■:Hmmmv•bla*ItoMvC/ッ》classAvilUPhotol"一"5HPhotoPhotoCoMectionpXPhotoFormftイPhctoUbrary■公PhotoUbfaryO4taI・例MormIDIS

58图2-2:类的XML文档注释信息在智能感知系统中的反映团不仅如此,在其他ー些相关的位置也可以得到提示,例如在将鼠标停留在其他引用“Photo”类型的代码上方:/I特タ片值風風步至na・///(/“Beary》l«Ch''.加二,か,gila;トニ•,二w■一ユ、bo]♦■:晏同钟姆ル」,=■yv^liettalieSybchFrT・2t(〃:ay—・い”at、1•»《«1•3.い=〃者无去RO柏USEれ用文件,后一次総改日難代,I»,»7"'尸!dt«nD«t««i»«41«>C*tL*«ttr>t«TiM,图2-3:XML文档注称信息会出现在工具提示中国し因此,只要是有可能在其他地方被使用到的类型或成员,都应该带有XML文档注释,以便通过智能感知系统向开发人员提供即时的参考信息。这些必要的位置包括:所有公开的类型、这些类型的所有公开的和受保护的成员、这些成员所涉及到的所有参数、返回值及可能抛出的异常。相反,命名空间本身以及成员函数的内部则不应使用XML文档注释,而应使用普通注释。XML文档注释标记中,有些是必须的,有些是可选的。我将在本节中逐个说明每种元素应当使用哪些XML文档注释标记,以及如何措辞。2.3.1结构与类的XML文档注释结构与类在XML文档注释的使用方式上是完全一致的,我们只需在summary标签中描述该类型的用途,请参考下面的代码:代码示例2-14;结构与类的XML文档注释样例III

///表示一幅照片。IIIpublicclassPhoto...由于编译器在处理XML文档注释的时候,会把相应的声明一起编译,因此我们不需要在注释文字中再强调这是ー个类或是ー个结构,只需耍描述它的用途。下面的几种描述方式都是比较好的,建议尽量进行套用:

59表示……。如果该结构或类是真实世界中的某个物体或抽象概念的直接反映,且其名称本身足以让人理解它的主要用途时,可以使用这种表述方式。如:“表示Windows按钮控件。”必要时也可以使用较为清晰的定义,如:“表示在二维平面中定义的、整数X和Y坐标的有序对。”提供……。如果该结构或类的功能更倾向于提供某种功能或服务,或者其本身的抽象概念名称无法清晰表达它的主耍用途时,可以使用这种表述方式。如:“提供创建图形缓冲区的方法。”或者“提供用于构造电子邮件的属性或方法。”包含……。有些类型本身并没有包含功能,它的主要目的就是提供对其他ー些工具或对象的访问,此时可以使用这种表达方式。如:“包含所有标准颜色的笔刷对象。”在XML注释中,我们只需要写出这个结构或类是什么,让使用者明白它的用途即可,不必将结构或类内部的工作方式公布出来,这会破坏类的封装性。毕竟其内部如何工作是设计者的事情,而不是使用者所需要关心的。普通注糅的读者是源代码的读者,而XML注释的读者是类的使用者。仅在两种情况下,我们有必要将内部的工作方式透露给使用者。ー种是当多个功能相似的类并存的时候,其主要区别即在于内部的工作方式。例如同样都是表示“集合”的类,ー个是用数组实现,一个是用链表实现,ー个是用双向链表实现,ー个是采用哈希表实现等等。如果单独考虑每ー个类,在注释的时候都没有必要解释其工作方式,而当它们同时存在的时候,就必须将具体的实现方式公开出来。另ー种情况是,如果使用者不知晓内部的某些具体处理方式,可能会导致严重的性能问题或者是安全性问题。对于结构与类来说,只有summary这ー个必备的标签,至于其他可选标签的使用,我将在本节稍后的时候集中提到。2.3.1属性的XML文档注释属性(Property),与结构与类相似,也只有summary这一个必须的XML文档注释标签,用于描述这个属性的作用。例如:代码示例2-15:属性的XML文档注释样例///

//Z获取或设置描述信息。///

60publicstringDescriptiongetreturnPhotoLibraryData.GetDescription(origina1Filename);setif(supportsMetadata==true)imageMetadata.Description=value;在编辑器的智能感知系统中反映如下:〃皑漳・Six«PatriM4,H=«

61有时,以“获取(或设置)”开头可能在语言组织上会发生困难,这通常发生在bool类型或枚举类型的属性上。此时则可以使用“获取(或设置)ー个值,该值指示……”的方式进行阐释。如:“获取或设置ー个值,该值指示控件是否可以对用户交互作出响应。”或是“获取ー个值,该值指示控件是否包含一个或多个子控件。”2.3.3方法的XML文档注释相比而言,与方法(Method)相关的XML文档注释要复杂的多。不但需耍用summary标签添加总体描述,还要用param标签为每个参数(Argument)进行详细的说明,并用returns标签描述函数的返回值(ReturnValue)〇下面的示例展示了一些标签的用法:代码示例2-16!方法及其参数与返回值的XML文档注释样例III

///返回指定图像文件的艺术家信息。IIIIII要检索的照片文件名。く/param>II!〈returns》返回指定图像文件的艺术家信息,如果没有找到,则返回空引用。く/retums>publicstaticstringGetArtist(stringfilename){LibraryDataSet.PhotosRowrow=Find(fi1ename);returnrow.Artist;}由于方法通常总是为了进行某种操作,因此在为其添加XML文档注释的summary标记时,并没有什么固定的格式,只需要采用动宾结构的短语对其功能进行描述即可。如:''在指定路径中创建文件。”如果方法的主要目的是为了获取某个特定的值,那么请使用“返回……”的句式,避免使用“获取”ー词,以与属性的描述相区分。如:“返回指定路径字符串的文件名和扩展名。”与结构与类的描述相类似,在为方法添加描述时,除非有非常好的理由,否则不要将方法的具体内部工作细节透露出去。

62//1///>MArtiitr•fg:JHl»)«二つ名Artttl«图2-5:参数的XML文档注称信息在智能感知系统中的反映[ini方法可能会带有参数,那么应当使用param标记为每ー个参数添加XML文档注释。它的内容将直接反映在智能感知系统中,向开发人员作出提示(如图2-5)。在为参数书写XML文档注释时,要采用偏正结构的短语清晰地描述该参数在整个函数方法中的功能及影响,不要仅仅从参数名称的字面上进行描述,那样可能无法提供真正有意义的内容。例如上例中,将filename参数标注为“要检索的照片文件名”就比単单ー个“文件名”要清楚得多。对于有返回值的方法,我们必须使用returns标记详细解称其所有可能的返回值,以及它们代表的含义。很多情况下,方法除了正常的返回值以外,还会出现异常情况下的返回值。而后者则是我们真正需要描述的。因为如果一切正常,调用者很清楚该方法会返回怎样的结果,而如果过程中出现了意外(例如本要打开文件却没有找到,没有找到要查询的数据库记录等等),那么大家对于该方法会如何进行反应就不是那么清楚了。究竟是返回一个空值,还是返回一个默认值,还是引发ー个异常?这些都需要我们加以说明。前两种情况下,我们都需要在returns里直接描述。例如上例中的“如果没有找到,则返回空引用”之类的表述方式。这里值得注意的是,在C#中编程与在C++或者Java中有一个明显的不同,即调用你开发的组件的很可能不是C#程序,而是VB.NET、C++.NET甚至是J丸在.NET中,你必须随时考虑除你当前使用之外的其他语言。对于VB.NET程序员来说,“nuU”是ー个没有意义的符号,“空引用”在VB.NET中是用Nothing关键字来表示的。因此,当我们在书写XML文档注释的时候,应避免写入与具体语言相关的信息。2.3.4构造函数的XML文档注释构造函数(Constructor)虽然不是方法,但它与方法的形式类似,XML文档注释的使用方法也基本相同。在其描述上,可以采用ー些模式化的表达方式:

63初始化……类的新实例。对于没有参数限定的构造函数来说,这种表述方式是最简单的。它仅仅是告诉使用者,这是ー个没有任何特别之处的构造函数,它将以默认的方式构造ー个新实例。例如:“初始化System.Web.UI.Page类的新实例。”使用指定的……初始化……类的新实例。当构造函数需要参数,且该参数是起某种限定作用(通常是对某个属性的直接设置)时,即可使用这种形式的描述。例如:“使用指定的表名初始化System.Data.DataTable类的新实例。”基于……初始化……类的新实例。当构造函数需要参数,而该参数提供的是核心组件,整个类的一切都是围绕该组件工作或是对它的进一步包装时,建议使用此种描述。例如:“基于所提供的流,用UTF-8作为字符串编码来初始化System.10.BinaryWriter类的新实例。”上面给出的只是最基本的描述主句,只要有必要,就可以(而且应当)在后面附加更详细的描述,尤其当构造函数具有多个重载(Overloads)时更是如此。该描述应当突出默认值是如何处理的、各个甫载版本之间有什么差别等等。以System.Collections.Generic.List类型为例,它的构造函数有三个重载版本:publicList();publicList(intcapacity);publicList(System.Collection.Generic.IEnumerablecollection);第二个重载中的capacity参数用于指定这个列表的初始容量,这很容易理解,但是如果在第一个帀载中仅仅是说“初始化List类的新实例”,那么使用者就会产生疑惑,他们无法得知第一个构造函数重载是如何处理初始容量的,是〇还是1?或者是其他默认值?.NETFramework是这样描述三个构造函数重载的:1.初始化ListくT>类的新实例,该实例为空并且具有默认初始容量。2.初始化List类的新实例,该实例为空并且具有指定的初始容量。3.初始化List类的新实例,该实例包含从指定集合复制的元素并且具有足够的容量来容纳所复制的元素。虽然并没有指出具体的数值[111,但比什么也不说要好得多。2.3.5事件的XML文档注释事件(Evenい成员自身只需要通过summary标记添加总体描述即可,我们建议严格地使用“当……时发生”的句式。例如:

64代码示例2-17:事件的XML文档注释样例III

III当照片的标题被更改后发生。IIIpubliceventTitleChangedEventHandlerTitieChanged;与事件相关的还有其委托(Delegate),提供事件数据的类以及引发事件的函数等等,它们也都需要添加XML文档注释。与事件ー样,它们也有着非常简单而确定的表述句式。下面即是ー个完整的事件所需的XML文档注释示例:代码示例2-18:一个完整的事件所需的XML文档注释样例IIIIII引发事件。IIIIII包含事件数据的oprotectedvirtualvoidOnTitleChanged(TitleChangedEventArgse)...IllIII表示将处理事件的方法。IIIpublicdelegatevoidTitleChangedEventHandler(objectsender,TitleChangedEventArgse)...IllII!为publicclassTitleChangedEventArgs:EventArgs...

65有关事件的处理、定义与引发,我将在第12章“事件与委托”(第222页)屮进行详细的讨论。2.3.5枚举类型的XML文档注释对于枚举类型(EnumerationType)来说,除了要给枚举类型本身添加summary描述之外,还要为每个枚举值添加summary描述:代码示例2T9:枚举类型的XML文档注释样例III

///表示测量距离的长度单位。IIIpublicenumUnit{IIIIII未知单位。IIIUnknown=0,IIIIII英寸。IIIInch=2,III//Z厘米。IIICentimeter=3}2.3.6泛型的XML文档注释与普通类型相比,泛型(GenericType)要多出一个或多个类型参数。添加XML文档注释的方式也很简单,只需在原有的基础上添加一个typeparam标记即可,表述上可采用“……的类型”的句型。例如.NETFramework中的Dictionary泛型类:

66代码示例2-20:泛型的XML文档注释样例III

III表示键和值的集合。IIIIII字典中的键的类型。III字典中的值的类型。[SerializableAttribute][ComVisibleAttribute(false)]publicclassDictionary:IDictionary,ICollection>,[EnumerableくKeyValuePairくTKey,TValue>>,IDictionary,ICollection,lEnumerable,ISerializable,IDeserializationCalIback...2.3.8其他标记如果在编译时打开了编译XML文档注释功能的话,那么之前介绍的那些标记与用法,都是会在编译时进行语法检查的。假如某个公开的类型或成员缺少summary标记,或者是缺少某个参数或返回值的注释,编译器都会给出警告信息。除此之外,还有一些标记不会经过任何语法检查,但仍然有着重要的作用。当你需要进行篇幅较长的解释说明的时候,summary标记显然是不适合的,它应该包含那些精炼的定义式说明。如果你希望向别人多提供ー些信息的话,可以选择放在remarks标记中。你还可以使用example标记添加示例,用code标记加入各种代码。“see”标记在前面的示例中已经出现过,当我们在XML注释中需要对某个类型进行引用的时候,不要直接写出类型的名称,而应该通过see标记进行引用,这样编译器就可以将其识别为ー个类型,以便在最终形成的文档中将其转换为链接或其他需要的形式。“see”标记

67只需要“href”ー个属性,其值即为类型的名称,可以是完全限定,也可以是不完全限定,只要保证没有二义性即可。例如:代码示例2-21:XML文档注释标记“くseeゾ的用法III

III使用指定的区分大小写设置初始化类的新实例。IIIIII是否区分大小写。protectedSearchCondition(boolcaseSensitive)(this.caseSensitive=caseSensitive;}当需要引用的类型为泛型时,需要注意一点,因为C#中标记泛型类型参数的尖括号在XML中属于特殊字符,我们需要按照XML的规则将“く”和“ゾ分别改写为“&属;”和“>”。例如:代码示例2-22;XML文档注释标记中特殊字符的写法IIIIII获取该照片集合的强类型迭代器。IIIIIIII!返回用于枚举的类型对象。IIIpublicIEnumeratorGetEnumerator()|foreach(Photophotoinphotos.Values)yieldreturnphoto;

68其中本来应写为【EnumeratorくPhoto)的现在只能写为IEnumerator<Photo>。这一点必须注意,否则将会引起XML文档注释编译失败。相似地,当需要引用某个参数的名称时,也不要直接书写,而应使用paramref标记进行引用,将参数名称作为name属性的值写入。而对泛型类型参数的引用,应使用typeparamref标记。当某个成员有可能抛出异常时,应使用exception标记将可能抛出的异常类型逐一列出,并解释抛出异常的主要原因:代码示例2-23:XML文档注释标记“くexceptionゾ的用法III

///初始化类的新实例,它表示指定路径的图像文件,并将元数据同步至数据库。IIIIII图像文件的路径和文件名。IIIIII当找不到くparamrefname="filename"/)参数指定的文件时发生。IIIinternalPhoto(stringfilename)…这些异常信息也会出现在智能感知系统的提示中(见图2-6):〃恰・重一itget”=(•1**)(〃创谭寛片?!金♦冏歩人釣權奪F!。32。い=〃正入列稟jPf'OtO.PhctCKftring4:A厶(れ1.2mHtPhoto七场吊定メ运・戈・ヌfえ皿胴ル至St再・r.torBk**I1.2-6:关于抛出异常的XML文档注释信息在智能感知系统中的反映口ゆ它将提示开发人员,可能需要对参数进行必要的检查,或者需要在try…catch…finally结构中进行操作。

69总的来说,如果多看看MSDN自身的类库参考,你会发现其实XML文档注释最终形成的就是这样的结果。MSDN本身就是ー个最好的XML文档注释的范例,我们可以在实践过程中不断地进行模仿和学习。第7章如何使用函数在面向过程开发中,程序是由函数组成的,可以说,在那个年代,函数就是应用程序的基石。函数与函数之间的关系也成了软件开发架构中极其重要的ー个方面。而现如今,在面向对象开发中,函数的地位已被类所取代,但它仍起着重要的作用ーー在类的两大组成部分(数据与操作)中,它占据了半壁江山。从另ー个层面上说,即使是面向对象开发,在具体细节层面上,面向过程仍是不可或缺的编程模式。本章将从面向过程的角度,详细讨论函数的使用。7.1为什么要使用函数很多开发人员都知道如何定义函数并调用它,但不见得每个人都能用好它。也许很多时候,只是为了使用函数而使用函数。其实,引入任何ー个新的概念或者技术之前,最关键的问题并不是如何使用它,而是为什么要使用它。只有深刻理解了使用它的初衷,オ有可能将其用得恰当。本节将讨论引入函数机制的几个主要理由。7.1.1函数与方法首先,我要先解释ー组容易被混淆的概念:函数与方法。它们实际上所指的东西几乎相同,但在不同的情景下我们会使用不同的术语。函数是与语句对应的概念,它是从编程语言本身的机制及语法结构角度来看的;方法则是与对象、属性、事件等平行的概念,它是从面向对象的功能角度来看的。在,#中,类/对象的方法是由函数来实现的,但并非所有的

70函数都是方法。事实匕属性最终也是由函数机制来实现的,虽然在语法上看起来有些区别,但其实质并没有改变。例如属性:privateintage=0;publicintAge(get{returnage;}set{age=value;})其实就可以视为下面两个函数的ー种语法上的整合:privateintage=0;publicintGetAgeO£returnage;}publicvoidSetAge(intvalue)Eage=value;)除此之外,有些函数创建的本意,并非是在面向对象中向类型提供新的方法,而是出于与面向过程编程中使用函数相同的理由。例如:privateboolVaiidateMonth(intmonth)Eif(1<=month&&month<=12){returntrue;else

71returnfalse;我们并不打算给当前类添加一个ValidateMonth方法,这只是供该类中的其他代码调用的ー个函数,这类函数虽然从语法上来说确实为当前类提供了一个方法,但从语义上来说,它只是一个普通的函数。通常来说,访问性为private的函数都不是真正的方法,因为它并不会对外提供可执行操作,而只是在类的内部被调用。7.1.2代码复用最早人们使用函数的目的,就是为了减少代码重复ーー这也是各类编程书籍中提及最多的理由。在程序中往往会遇到ー些需要多次使用的代码,它们有时是原封不动地重复出现,有时只有非常微小的变化。例如下面这段代码:graphics.DrawRectangle(newPen(Color.Black,2),8,8,150,23);StringFormatformat1=newStringFormat();format1.Alignment=StringAlignment.Center;formatl.LineAlignment=StringAlignment.Center;graphics.DrawString("BoxA",newFont("Verdana,9),newSolidBrush(Color.Black),newRectangleF(8,8,150,23),formatl);graphics.DrawRectangle(newPen(Color.Black,2),8,35,150,23);StringFormatformat2=newStringFormat();format2.Alignment=StringAlignment.Center;format2.LineAlignment=StringAlignment.Center;graphics.DrawString(〃BoxB〃,

72newFont("Verdana”,9),newSolidBrush(Color.Black),newRectangleF(8,35,150,23),format2);我们画了两个带有居中文本的方框,如果需要三个或者更多的话,复制代码不但显得笨拙,而且不便于代码修改。循环可以在一定程度上解决这类问题,但我们不得不特别创建一些数组或集合来向循环提供数据。此时最好的办法就是使用ー个函数:代码示例7-1:使用函数复用代码privatevoidDrawBoxWithText(intx,inty,intwidth,intheight,ColorforegroundColor,intborderThickness,stringtext,Fontfont)Igraphics.DrawRectangle(newPen(foregroundcolor,borderThickness),x,y,width,height);StringFormatformat=newStringFormat();format.Alignment=StringAlignment.Center;format.LineAlignment=StringAlignment.Center;graphics.DrawString(

73text,font,newSolidBrush(foregroundColor),newRectangleF(x,y,width,height),format);)当需要绘制时,调用这个函数即可:(续代码示例7-1:使用函数复用代码)DrawBoxWithText(8,8,150,23,Color.Black,2,"BoxA","Verdana",9);DrawBoxWithText(8,35,150,23,Color.Black,2,"BoxB","Verdana",9);如果我们需要修改绘图方式时,只需一次性修改函数,就算需要添加更多的参数,编译系统也会帮助我们找到所有调用这个函数的位置,不至于出现遗漏。7.1.3隐藏细节你也许会不相信,但时至今日,函数最主要的目的(除了向类提供方法之外)并非代码复用,而是隐藏细节。这可能与面向对象中的抽象有一点相似,不过你很快就能体会到它们的差别。先看下面的这段程序:publicvoidProcessDataFile(stringsourceFile,stringdestinationFile)(Listくint〉numbers=newList();FileStreamfs=newFileStream(sourceFile,FileMode.Open,FileAccess.ReaBinaryReaderr=newBinaryReader(fs);

74trywhile(r.PeekChar()!=-1)(numbers.Add(r.Readlnt16());I)finally{r.Close();}fs.Close();intsum=0;foreach(intnumberinnumbers){sum+=number;)doubleaverage=(double)sum/numbers.Count;fs=newFileStream(destinationFile,FileMode.OpenOrCreate);BinaryWriterw=newBinaryWriter(fs);w.Write(average);

75w.Close();fs.Close();)这段程序看起来很乱,它其实主要做了三件事:先从源文件中读取ー组数据,然后计算平均值,最后将计算结果写入目标文件中。既然我们在思考的时候首先是将其分解为三件事,而不是每ー个细节步骤,那么程序也应当尽可能地反映出这种逻辑,于是我们可以将这三件事分别写成三个函数:代码示例7-2:使用函数进行抽象publicvoidProcessDataFile(stringsourceFile,stringdestinationFile)(Listくint>numbers=ReadDataFromFile(sourceFile);doubleaverage=GetAverage(numbers);WriteDataToFile(destinationFile);)privateListReadDataFromFile(stringsourceFile){Listnumbers=newList();FileStreamfs=newFileStream(sourceFile,FileMode.Open,FileAccess.Read);BinaryReaderr=newBinaryReader(fs);tryjwhile(r.PeekChar()!=-1)numbers.Add(r.Readlnt16());

76finallyr.Close();Ifs.Close();returnnumbers;)privatedoubleGetAverage(Listnumbers){intsum=0;foreach(intnumberinnumbers)Isum+=number;}doubleaverage=(double)sum/numbers.Count;returnaverage;)privatevoidWriteDataToFile(stringdestinationFile,doubleaverage)(fs=newFileStream(destinationFile,FileMode.OpenOrCreate);BinaryWriterw=newBinaryWriter(fs);

77w.Write(average);w.Close();fs.Close();)很显然,经过上述处理之后,ー个问题被分解为三个子问题,代码的复杂性降低,犯错误的机率也同时得以减少。7.2函数重载函数重:载い31(Overload)为程序开发提供了极大的便利,尤其是对于那些类库的使用者ー他们可以从复杂的类型转换中解放出来,只要以他们自己认为最方便的方式调用方法即可。本节主要从重载的语义角度来讨论与使用重载相关的ー些基本准则。7.2.1重载的语义先让我们看看在不支持重载的语言中会发生什么:在C语言中,math,h头文件包含了如下几个函数的声明:intabs(intx);longlabs(longx);doublefabs(doublex);structcomplexcabs(structcomplexx);这四个函数的功能都差不多,都是取绝对值,它们的区别即在于面向不同的数据类型:abs用于一般整型数,labs用于长整型,fabs用于浮点型,cabs用于复数结构。为了支持四种不同的数据类型,C语言库函数不得不提供了四个不同名称的函数(事实上还有一些数据类型是通过隐式类型转换实现的,如short、float、char类型等等)。开发人员的记忆负担也明显加重,因为他们必须记住四个函数名称,而事实上这四个函数并没有本质区别!重载就是为了解决这个问题而产生的,既然它们实际上是同一个函数,那么就使用同一个名称:intabs(intx);longabs(longx);doubleabs(doublex);

78具体应该调用哪・个函数是可以通过调用方给出的参数来确定的。从上面的解释中,我们可以知道,虽然不同的重载在形式上是多个独立的函数,但在语义上它们代表的是同一个函数——准确地说,它们执行的是同样的操作。之所以函数要提供多个重载,其主要目的是方便调用者。具体地说来包括以下一些情形:1.支持多种数据类型前面关于abs函数的例子即是说明了这一点。我们要提供的是“取绝对值”的功能,并不是“对整型数取绝对值”的功能,因此将输入参数限制为整型是不合适的。提供重载可以使得用户不必操心有关数据类型转换的工作,各个重载函数会负责针对不同的数据类型进行处理。2.支持多种数据提供方式这是比兼容多种数据类型更为宽泛而方便的方式。整型与长整型之间可以直接转换,但很多情况却没有那么简单。例如XmlDocument类提供了Load方法用于装入ー个XML文档,那么如何指定这个文档呢?如果这个文档还没有被打开,那么指定它的路径及文件名是最方便的。如果这个文档已经打开,那么也许可以通过ー个Stream对象対其进行访问,也许是轻量级的TextReader,也许是专门的XmlReader。对于类库的使用者来说,这些可能都是存在的,如果XmlDocument强制使用ー种方式,无疑会给用户造成不便,此时最好的办法就是提供ー组重载:XmlDocument.Load(string)XmlDocument.Load(Stream)XmlDocument.Load(TextReader)XmlDocument.Load(XmlReader)这里的string、Stream、TextReader和XmlReader之间是不存在类型转换关系的,重载函数要做的额外工作并不像之前那个绝对值函数那么简单。但是对于函数的使用者来说,它们的意义是类似的。3.为复杂的参数提供默认值,简化调用这也是非常常见的情况。有些函数的功能非常强大,但其带来的负面影响就是函数调用者需要考虑太多的参数设置。事实上,大多数情况下,使用者真正关心的只是其中的ー两个参数,剩余的参数都集中在某些相同的设置值上。如果能为这些高级特性提供合理的默认值的话,

79函数调用者的工作量就会大大降低。例如Graphics的Drawlmage方法用于绘制-•幅指定的图像,它支持很多丰富的设置参数:publicvoidDrawlmage(Imageimage,RectangledestRect,floatsrcX,floatsrcY,floatsrcWidth,floatsrcHeight,GraphicsUnitsrcUnit,ImageAttributesimageAttrs,DrawImageAbortcallback,IntPtrcallbackData}我第一次知道在屏幕上绘制图片还需要这么多参数时完全不知所措、在我看来,只要指定图片和要显示的座标位置就够了ーー就像下面这样:publicvoidDrawImage(Imageimage,intx,inty}事实上Drawlmage确实提供了这样ー个简单版本的重载,确实在大多数情况下,用户只需要ー些简单的功能,那些复杂的特性(图片缩放显示、裁剪显示、图像调整等)被使用的频率相对较低。如果每次都要调用者设置“不缩放”、“不裁剪"、"不进行图像调整”的话,显然增加了很多不必要的劳动。如果提供ー个简单版本的重载,为这些高级特性提供合理的默认值的话,可以在很大程度上使代码变得更加简洁有序。前面这个重载函数,或许就是通过下面的方式来实现的:publicvoidDrawImage(Imageimage,intx,inty)

80Drawlmage(image,newRectangle(x,y,image.Width,image.Height),0,。,image.Width,image.Height,GraphicsUnit.Pixel,null,null,IntPtr.Zero可以清楚地看到,如果没有这些帀载来帮助设置默认参数值的话,用户需要多编写多少代码去完成一个本应该非常简单的操作。事实上,Drawlmage总共提供了30个重载:Graphics.Drawlmage(Image,Point)Graphics.Drawlmage(Image,Point[])Graphics.DrawImage(Image,PointF)Graphics.DrawImage(Image,PointF[])Graphics.Drawlmage(Image,Rectangle)Graphics.DrawImage(Image,RectangleF)Graphics.Drawlmage(Image,int,int)Graphics.Drawlmage(Image,float,float)Graphics.Drawlmage(Image,Point[],Rectangle,GraphicsUnit)Graphics.DrawImage(Image,PointF[],RectangleF,GraphicsUnit)Graphics.Drawlmage(Image,Rectangle,Rectangle,GraphicsUnit)Graphics.DrawImage(Image,RectangleF,RectangleF,GraphicsUnit)

81Graphics.Drawlmage(Image,int,int,int,int)Graphics.DrawImage(Image,int,int,Rectangle,GraphicsUnit)Graphics.Drawlmage(Image,Point[],Rectangle,GraphicsUnit,ImageAttributes)Graphics.Drawlmage(Image,PointF[],RectangleF,GraphicsUnit,ImageAttributes)Graphics.Drawlmage(Image,float,float,RectangleF,GraphicsUnit)Graphics.Drawlmage(Image,float,float,float,float)Graphics.Drawlmage(Image,Point[],Rectangle,GraphicsUnit,ImageAttributes,Graphics.DrawlmageAbort)Graphics.Drawlmage(Image,PointF[],RectangleF,GraphicsUnit,ImageAttributes,Graphics.DrawlmageAbort)Graphics.Drawlmage(Image,Point[],Rectangle,GraphicsUnit,ImageAttributes,Graphics.DrawImageAbort,int)Graphics.DrawImage(Image,PointF[],RectangleF,GraphicsUnit,ImageAttributes,Graphics.DrawlmageAbort,int)Graphics.Drawlmage(Image,Rectangle,int,int,int,int,GraphicsUnit)Graphics.Drawlmage(Image,Rectangle,float,float,float,float,GraphicsUnit)Graphics.Drawlmage(Image,Rectangle,int,int,int,int,GraphicsUnit,ImageAttributes)Graphics.Drawlmage(Image,Rectangle,float,float,float,float,GraphicsUnit,ImageAttributes)Graphics.Drawlmage(Image,Rectangle,int,int,int,int,GraphicsUnit,ImageAttributes,Graphics.DrawImageAbort)Graphics.DrawImage(Image,Rectangle,float,float,float,float,GraphicsUnit,ImageAttributes,Graphics.DrawImageAbort)

82Graphics.Drawlmage(Image,Rectangle,int,int,int,int,GraphicsUnit,ImageAttributes,Graphics.DrawlmageAbort,IntPtr)Graphics.Drawlmage(Image,Rectangle,float,float,float,float,GraphicsUnit,ImageAttributes,Graphics.DrawImageAbort,IntPtr)这么多的帀载其实包括了我们之前说的所有三种情况,既有用于不同数据类型的(如int与float的转换),也有用于不同的数据提供方式的(如分别提供x、y、宽度、高度值,或者是提供Point数组,或者是提供Rectangle实例等),也有是为了隐藏ー些复杂的参数设置的。对于功能强大而又极为常用的函数方法来说,尽可能多地提供而载以便利调用者(也许它的调用者就是你自己)是很有必要的。7.2.2保持核心代码唯一从之前所述的帀载语义中,我们不难发现,函数的各个重载之间的区别仅在于对入口参数进行的预处理,其核心功能都是一致的。如果每个重载都单独编写的话,必然会产生大量的冗余代码,不但增加了编写代码的工作,也给日后的修改维护造成极大的困难。既然提供重载只是为了处理不同的入口参数形式,那么各个重载函数的工作也就应该是对参数进行处理,核心功能应该只由一个函数实现,其他所有重载都调用这个函数。我们以ー个GetPhotosByDate函数为例,它的功能是返回指定的日期范围内的照片:代码示例7-3:GetPhotosByDate函数原型publicPhotoCollectionGetPhotosByDate(PhotoLibrarylibrary,DateTimestartDate,DateTimeendDate)(/Z获取筛选结果string[]ids=PhotoLibraryData.GetPhotoIds(*(TakenDate>=ダ+startDate.ToLongDateStringO+

83”#ANDTakenDateく#"+endDate.ToLongDateStringO+"#)",

84null,false);/Z存储筛选结果的照片集合photos=newPhotoCollectionO;/Z填充foreach(stringidinids){photos.Add(library.Photos[id]);)photos.LockO;returnphotos;)这个函数使用startDate和endDate两个DateTime类型的参数来定义日期范围,表示从startDate开始到endDate之前(不包括endDate在内)的日期范围。但很多时候,日期并没有被转换成DateTime类型提供,因此我们提供了一个相似的重载版本,用年月日分列的形式代替了DateTime类型:代码示例7-4:使用简单数据类型的重载publicPhotoCollectionGetPhotosByDate(PhotoLibrarylibrary,intstartYear,intstartMonth,intstartDay,intendYear,

85intendMonth,

86intendDay)returnGetPhotosByDate(1ibrary,newDateTime(startYear,startMonth,startDay),newDateTime(endYear,endMonth,endDay));}虽然参数的个数一下子增加了,但其实并没有影响调用的复杂度,直接使用分开的年、月、日数值是符合我们的习惯的,尤其当用户是通过不同的控件单独输入年月日数值时。继续深入考虑其他的情况:人们在根据日期查看照片时,往往不会精确到XX年X月X日,而往往是指定某个月或者某几个月的照片,为了更好地支持这样ー种模式,我们又添加了几个重载:代码示例7-5:更多直接处理年份与月份参数的重载/Z仅需指定起始年月和结束年月的重载版本publicPhotoCollectionGetPhotosByDate(PhotoLibrarylibrary,intstartYear,intstartMonth,intendYear,intendMonth)|returnGetPhotosByDate(1ibrary,newDateTime(startYear,startMonth,1),newDateTime(endYear,endMonth,1).AddMonths(1)

87/Z仅需指定单个年份与月份的市載版本publicPhotoCollectionGetPhotosByDate(PhotoLibrary1ibrary,intyear,intmonth)(returnGetPhotosByDate(1ibrary,newDateTime(year,month,1),newDateTime(year,month,1).AddMonths(l));)〃仅需指定単个年份的重载版本publicPhotoCollectionGetPhotosByDate(PhotoLibrary1ibrary,intyear){returnGetPhotosByDate(1ibrary,newDateTime(year,1,1),newDateTime(year+1,1,1)

88我真正希望大家注意的是:除了第一个重载版本中包含了实际处理代码之外,后面的重载都只是対参数进行处理后再次调用第一个重载。也就是说,只要参数的形式不影响核心代码的工作方式的话,那么简单的用载版本的工作就是调用复杂的用载版本,而真正的工作应该让最复杂的那个重载版本来完成。7.3参数的设计参数是函数的ー个而要组成部分,本节与下ー节都将着帀讨论与参数相关的话题。虽然看上去,参数的使用仅取决于函数本身工作所依赖的信息,但是设计欠佳的参数往往会给调用者带来许多不便,随着项目规模的增长,开发的难度与复杂度也越来越大。7.3.1参数的命名对于函数体来说,参数相当于ー个隐含声明的局部变量,因此它也应当遵循局部变量的一切准则。在命名上,参数应使用Camel方式大小写,并使用描述性的、有明确意义的名称。例如:代码示例7-6:参数的一般命名准则publicvoidSend(stringfrom,stringrecipients,stringsubject,stringbody)好的参数命名可以省去开发人员查阅文档所消耗的大量时间,VisualStudio的编辑器在开发人员书写代码时会自动提示函数各参数的类型与名称,如果命名恰当,开发人员就能够迅速理解应该传递何种数据。7.3.2不要使用保留项过去,为了与未来的版本保持尽可能的兼容,开发人员会在能够预见到改变的函数参数中添加保留项。例如WindowsAP!中的TrackPopupMem!函数:BOOLTrackPopupMenu(HMENUhMenu,UINTuFlags,

89intx,inty,intnReserved,HWNDhWnd,CONSTRECT*prcRect);它包含了一个名为nReserved的保留参数,调用时必须传入。;另外还包含了一个名为preRect的参数,没有任何作用,函数会完全忽略它——这两个参数对于调用者来说都是完全没有意义的。这在当时为了追求最大兼容性是可以被接受的行为,否则新版本的函数一但需要增加新的参数就不得不另外命名为TrackPopupMenuEx或者TrackPopupMenu2之类的名称。但在C#已经支持函数重载的情况下,这么做就不那么合适了。新版本的函数仍然可以继续维持原来的名称,只需提供更多参数的重载版本就可以了。7.3.3变长参数列表从理论上来说,变长参数列表并不是必须的,因为完全可以用数组来解决数据个数不定的问题,但从编写代码的角度来说,变长参数列表确实可以提供一定的便利。通常来说,当函数只会用到数量不多的几个参数(绝大多数情况下不超过七个)时,可以考虑使用变长参数列表。例如,我们编写ー个可以从多个日期中选择中最迟日期的函数:代码示例?-7:使用变长参数列表的GetLastestDateTime函数publicDateTimeGetLastestDateTime(paramsDateTime口values)(DateTimelastestDateTime=DateTime.MinValue;if(values!=null)(foreach(intvalueinvalues)lastestDateTime=(lastestDateTime

90returnlastestDateTime;当需要从多个日期中取出最大数值的时候,就可以轻松地在函数调用时列出所有的项:DateTimenewestDateTime=GetLastestDateTime(creationTime,lastAccessTime,lastWriteTime);变长参数列表的ー个重要特性是:列表中的所有数值的意义都是相同的,它们之间是完全平等的。这一点非常重要,像ド面的这个函数就错误地使用了变长参数列表:publicvoidCopyFiles(paramsstring[]fileNames)(if(fileNames==null)(return;)inti=0;while(i

91然而,即使参数之间确实不存在任何组合关系,同样是一次性处理并列的多个数值,下面所示的函数仍然可能存在问题:publicvoidDeleteFiles(paramsstring[]fileNames)(if(fileNames==null)(return;}foreach(stringfileNameinfileNames)(File.Delete(fi1eName);}}其实问题不在于这个函数自身,而在于调用这个函数的情形。対于同时删除多个文件这样的操作来说,绝大多数情况下都是由使用最终软件的用户发起的。也就是说,对于开发人员来说,需要删除的文件名都会以数组或集合的形式存在,根本不会需要用到变长参数列表。也许您已经发现前面的示例中都对变长参数列表项自身进行了空值检査,这是必要的,因为其他开发人员完全可以将null作为参数值传给变长参数列表项。7.3.4何时使用ref参数和。ut参数有一条根本的准则是:尽量避免使用ref参数或out参数。这需要调用它的开发人员对值类型、引用类型、甚至是指针等概念有着深入明确的理解。而且,通过参数返回函数结果在C#中也是不合乎习惯的。如果这个参数值来源于某个对象的属性,那么开发人员不得不额外定义・个临时变量来用于获取执行结果,因为属性的实质是函数,而并非变量,不能以指针的方式进行传递。很多时候,使用ref或out参数的初衷是因为函数需要返回多个值。以搜索函数为例,如果函数只是返回搜索出来的条目,那么只要返回一个数组或集合就可以了:publicList

92Search(string[]keywords,intpageindex,intitemsPerPage)

93但如果我们需要添加更丰富的功能,当输入的关键字存在可能的拼写错误时,让函数能够返回ー个更正的或者推荐的关键字。此时,单个的返回值无法实现,我们很容易想到的是借助ref参数或者out参数:publicListSearch(string[]keywords,intpageIndex,intitemsPerPage,outstring[]suggestedKeywords)这样确实可以凑效,但调用者不得不事先声明ー个变量用来存放suggestedKeywords参数传出的值——事实上这个参数返回的值很可能并不常用。更加合理的方法是,定义ー个结构或类,作为函数返回的数据类型,而所有要返回的信息都作为这个类型的成员。例如:代码示例?-8:使用专门的类型作为函数的返回类型publicclassSearchResult(publicListItems{get;}publicstring[]SuggestedKeywords{get;})函数的调用者可以很容易地获取SearchResult类型的返回值,开发人员可以随意访问他所关心的信息,对于不需要的内容,简单忽略即可。7.3.5参数的顺序参数的顺序并不会造成程序运行时的实际差别,但总的说来,总是习惯于将核心数据放在前面,然后是相关的设置参数、选项、功能开关等,并将相关的参数靠在ー起,尽可能地符合人的阅读和思维习惯。当函数中带有ref参数、out参数或变长参数列表时,则有一些习惯与规则需要遵守。对于函数来说,正常的参数是用于输入信息的,而。ut参数是用于输出信息的,ref参数则两种功能兼有。从先有输入オ能有输出的思维顺序来看,我们应当将普通参数放在前面,随后是ref参数,最后

94オ是out参数——无论这些参数之间的关系如何,都应当如此排列。

95变长参数列表由于其自身的特殊性,只能位于函数的最末。7.3.6重载函数的参数一致在前一节中,我们已经讨论过重载函数的基本设计,从语法上来说,参数差异是函数各个重载之间的最本质的区别。然而,事实上参数在各个重载之间的差异往往很小,因此保持其一致性是非常重要的。重载函数的参数一致性主要体现在两个方面:4.名称的一致性如果同一个参数在多个重载版本中都存在,那么其出现在每个重载中的参数名称都应当是完全一致的。例如File.Create方法,它具有四个重载:代码示例7-9:重载函数参数的名称一致性publicstaticFileStreamFile.Create(stringpath);publicstaticFileStreamFile.Create(stringpath,intbufferSize);publicstaticFileStreamFile.Create(stringpath,intbufferSize,FileOptionsoptions);publicstaticFileStreamFile.Create(stringpath,intbufferSize,FileOptionsoptions,FileSecurityfileSecurity);可以看到,不同的重载只应在真正有所区别的参数上出现形式上的变化,如果两个参数表示的是同样的含义,那么就精确地使用相同的名称。5.顺序的一致性在名称一致性的基础上,函数不同重载之间,同一个参数在参数列表的相对位置应该不变。例如ArrayList类的BinarySearch方法具有如下三个重载:代码示例7-10:重载函数参数的顺序一致性

96publicvirtualintBinarySearch(objectvalue)publicvirtualintBinarySearch(objectvalue,System.Collection.IComparercomparer)publicvirtualintBinarySearch(intindex,intcount,objectvalue,System.Collection.IComparercomparer)下图清楚地展示了这三个重载中的参数顺序关系:BinarySearchvaluevaluecomparerindexcountvaluecomparerBinarySearchBinarySearch图7-1:正确的重载间参数顺序关系可以看到,如果把两个重载之间不同的参数拿掉的话,剩下的参数的顺序位置应该完全一致。相反,下面的参数定义则是错误的:publicFileStreamOpen(FileModemode)publicFileStreamOpen(FileAccessaccess,FileModemode)publicFileStreamOpen(FileModemode,FileAccessaccess,FileShareshare)这会使得参数顺序关系图中出现交叉:OpenOpenmodeaccessOpenshare

97图7-2:错误的重载间参数顺序关系这种顺序一致性还应扩展到“功能意义相同的参数项”上,而不仅仅是完全一致的参数,例如在对文件进行操作时,“文件名字符串”和“文件流对象”在功能意义上是相同的,因此在考虑参数顺序时,这两个参数应该被视为同一个参数来处理。例如:代码示例7-11:将功能意义相同的参数项视为同一个参数来参与顺序排列publicBitmap(stringfilename)publicBitmap(stringfilename,booluselcm)publicBitmap(System.10.Streamstream)publicBitmap(System.10.Streamstream,booluselcm)这里的filename和stream在概念上其实是同一个参数(见下图),因此如果将第四个重载中的stream和uselcm顺序调换是不可.取的。BitmapfilenameIBitmapfilenameuselcmIBitmapstreamIBitmapstreamuselcm图7-3:功能意义相同的参数项视为同一个参数让我们更进ー步:即使参数的个数不同,但只要它们在功能意义上相同,也应该被视为相同的参数。例如平面上的点既可以用Point类型来表示,也可以用ー对int值也表示;又如一个矩形既可以用Rectangle类型来表示,也可以用四个int类型的值来表示,或者用ー个Point和一个Size类型来表示等等。请看下面的ー组声明:代码示例7-12!功能意义相同的参数组合也应视为同一个参数publicvoidDrawlmage(Imageimage,intx,inty)publicvoidDrawlmage(Imageimage,Pointpoint)publicvoidDrawlmage(Imageimage,intx,inty,intwidth,intheight)Rectanglerect)publicvoidDrawlmage(Imageimage,publicvoidDrawlmage(Imageimage,floatx,floaty)

98publicvoidDrawlmage(Imageimage,PointFpoint)publicvoidDrawImage(Imageimage,floatx,floaty,floatwidth,floatheight)publicvoidDrawlmage(Imageimage,RectangleFrect)我仍然用图的方式来展现重载之间的参数关系:DrawlmageDrawlmageDrawlmageDrawlmageDrawlmageDrawlmageDrawlmageDrawlmage图7-4:功能意义相同的参数组合也应视为同一个参数如果单独只看第1、3、5、7个重裁,那么它们的顺序关系非常显而易见,虽然坐标可能是int类型也可能是float类型,但实质意义都是相同的,因此所在的顺序位置也应该是相同的。单独只看第2、4、6、8个重载也是如此。但如果将它们放在ー起,就可以看到int类型的参数x和y是如何合并成Point类型的参数point的,以及int类型的参数x、y、width和height是如何合并成Rectangle类型的参数rect的。対于这个函数来说,x、y、width和height这四个参数与rect这ー个参数是等价的,因此它们在参数列表中出现的位置应该是一致的,且不应再在四个参数之间插入其他参数。有一点需要提及的是,如果某个重载与其他重载相比缺少了bool类型的参数,那么该重载向这个参数提供的默认值应该是false〇这将与bool类型本身的初始值为false保持一致。如果true表示了更普遍的状况,那么请考虑使用反义词或其他方式来改变参数的含义。7.4参数检查就像要检查用户输入的信息是否合法一样,函数也需要对参数的值进行合法性检査,这直接决定了应用程序的健壮性。有些人可能觉得,我的函数都是由我自己的代码调用的,肯定不

99会传入非法的数值。其实当函数的数量增多之后,你自己也搞不清哪个函数里的数据是检查过的,哪些是没有检查过的。我在本节最后将讨论这个问题。7.4.1检查零值及空引用・对变长参数列表本身进行检査,它也有可能为null。大部分人肯定不会忘记检查月份是否在1至12之间,或者要打开的文件是否存在等等。因为这些数据的有效值范围显而易见,在编写代码时很容易受到重视。最容易被人遗忘的反而是最为普遍的地方,让我们来看下面这段代码:publicstaticSizeGetUniformSize(intimageWidth,intimageHeight,intcontainerWidth,intcontainerlleight){Sizeuniform=newSizeO;doubleaspectRatio=(double)imageWidth/imageHeight;doublecontainerAspectRatio=(double)containerWidth/containerHeight;if(aspectRatio>containerAspectRatio){uniform.Width=containerWidth;uniform.Height=(int)(containerWidth/aspectRatio);}else(

100uniform.Height=containerHeight;uniform.Width=(int)(containerHeight*aspectRatio);}returnuniform;

101这里既没有I/O操作,也没有数据库操作,一切都很平常,只是简单的数学运算,好像没有什么问题。其实不然,你每次看到除法运算的时候都应该像进行文件操作ー样谨慎!这个函数完全没有测试imageHeight和containerHeight是否为零就将其用做除数。除数为零是很多人容易忽略的问题,当你的程序中出现除法运算时,请加倍留心。空引用检查是很容易被忘记的,但对于ー些特殊的类型来说,空引用的威胁更加隐蔽。对于字符串来说,大多数人能够记得同时检查字符串是否长度为零以及其本身是否为null。.NET2.0更是提供了string.IsNullOrEmpty()方法专门用于同时进行这两种测试。但是对于数组和集合,开发人员员往往更加关心它所包含的元素个数,而忽略了对象本身是否为空:if(employees.Count>0)firstEmployee=employees[0]:如果employees本身就为null,那么在代码的第一行处就会产生异常。7.4.2检查枚举类型枚举类型也要被检查,因为允许任何整型数值被强制转换为某种枚举类型,无论这个值有没有被定义过。枚举类型并不像我们想像中的那么安全,这也是我在前面为什么要求当switch语句列出ー个枚举中所有定义过的项之后仍然要用default子句:代码示例7-13;对枚举类型参数进行检查publicboolIsWorkDay(DayOfWeekday)switch(day)caseMonday:caseTuesday:caseWednesday:caseThursday:

102caseFriday:returntrue;caseSaturday:caseSunday:returnfalse;default:thrownewArgumentOutOfRangeException("day","unexpectedvalue");))DayOfWeek枚举中定义的所有项都被列举出来了,你是不是觉得default子句多此ー举,永远不会被执行到?其实不然,我们完全可以突破这个界限:if(IsWorkDay((DayOfWeek)7))这种强制类型转换是完全合法的,我们很容易得到了一个值为7的DayOfWeek类型枚举值。然而DayOfWeek枚举中预定义的值是从。(Sunday)到6(Saturday)的,并没有7,因此它将会触发default子句中的异常。不要以为你不会这样写代码,这种事情就不会出现,你的枚举值有可能是从数据库或配置文件中读入的,这种强制转换将随处可见。你无法保证那些外部数据不会被有意或无意篡改,让你的程序自己来发现问题要比崩溃好得多。7.4.3防止数据被篡改当参数类型是可变类型时,应先制作一份私有副本,再进行进ー步的验证和处理,尤其是安全性非常重要时。因为参数引用的对象内容也许会在经过验证后被改变。数据通过参数传递到函数中之后,并不意味着函数就对它拥有绝对的控制权。对于大量的引用类型来说,函数外部的程序仍然能够随意对其进行任何操作。这样ー来,就可能会存在安全隐患。例如下面一段示例程序:publicboolSendMeMoney(AccountInfoaccount,intamount)(if(HasPaid(account.Id)=false)SendMoneyTo(account.Id,amount);

103returntrue;else(returnfalse;}}程序先检查是否向指定的帐户支付过,如果还未支付,则向这个帐户付钱。在正常情况下,程序在调用HasPaid和SendMoneyTo方法时,使用的account.Id应该是同一一个帐户号。但由于account是ー个引用类型的参数,整个函数在执行的过程中,外部代码仍然可以篡改它的值。这样ー来,别有用心的人可以将一个还未支付过的帐户信息输入,等HasPaid函数执行完毕之后,再将Id替换成自己的帐户,以获取非法的重复支付。对于这类安全性要求非常高的函数来说,应先为所有的可变参数创建一份副本,然后再进行验证和使用,而不是每次都重新获取源数据,以防非法篡改:代码示例7T4:在安全性较为敏感时,为可变参数制作一份私有副本publicboolSendMeMoney(AccountInfoaccount,intamount)(stringid=account.Id;if(HasPaid(id)==false)(SendMoneyTo(id,amount);returntrue;)elsereturnfalse;

104这样可保证验证和实际处理时针对的是同样的数据,外界对account数据的更改不会影响函数局部变量id的值。值得注意的是,尽可能地复制元数据,而不是整个对象。如果一定要复制整个对象或者当某个具体数据项自身也是可变类型时,一定要进行数据克隆,而不只是简单的更制引用。7.4.4在何处检查合法性之前讨论了参数检查的各种情况,那么究竟何时对参数进行检查?从用户输入数据直到最后数据被真正进行处理,往往要经过好几个函数层层传递。在每个函数中都进行合法性检查必然是谨慎安全的,但并不是所有的合法性检查都像检査月份是否是1至12之间的整数那样简单。有些合法性检查可能意味着要访问硬盘屮的文件、数据库中的信息、甚至是网络上的资源,如果每层函数都检查一遍,无疑是对资源的极大浪费。如何保证检查既不重复也不疏漏呢?有一种做法是将所有的函数分为安全区和非安全区,中间设立一层隔离带。这些非安全区的函数只能调用隔离带的函数,当隔离带验证了数据的有效性之后,才能将数据进ー步传送到安全区的函数。安全区的函数将不再进行任何数据有效性检查,它们视所有传入的参数都是合法有效的。这种做法在逻辑性较强,但必须区分准确,一旦出现某个函数划错区,或者某个函数忘记经过隔离带而直接访问了非安全区时,就会出现错误甚至导致安全漏洞。为了减少这类错误的发生,可以通过类型或函数的可访问性来实现这种强制控制:可访问性为public或protected的成员都属于非安全区,因为外界可以直接向其注入数据。而可访问性为interna!或private的成员都属于安全区,所有的数据实际操作都在internal/private成员内进行,公开成员仅能通过调用私有成员来完成任务。当成员调用仅在public/protected成员之间或者是仅在internal/private之间进行时,可以不必考虑数据安全性;而当public/protected成员调用internal/private成员时,必须进行安全检查。

105外部输入图7-5!通过可访问性区分安全区与非安全区内的成员另一一种方案的规则更为简单:仅在数据最终将被实际使用产生实际影响时オ进行合法性检査。7.5函数出口离开函数有三种方式,ー是执行完函数中最后一行代码,自然结束:二是通过retun返回值后离开(对于void函数来说即是无返回值);三是抛出异常并强制退出。作为本章的最后ー节,本节将讨论一些有关离开函数的建议。7.5.1返回值的用法在有些语言中,人们将有返回值的子程序称为“函数”,无返回值的子程序称为“过程”。在C#中,只有’‘函数"ー种,它既可以有返回值,也可以没有返回值。那么,函数为什么要有返回值?最显而易见的原因是为了返回计算结果,例如System.Math类中的所有函数,都是为了返回某种数学计算的结果。除了数学计算之外,也可能是获取计算机系统的当前时间,检查员工数据库中这个月有多少条迟到记录,或者是返回用户输入的个人全名,甚至是下载ー个网页的内容等等。另一种情况是返回操作执行状态,通常情况下返回值的类型是bool或者特定的枚举类型。例如用于删除一条记录的函数应当返回一个状态值,告诉调用者该记录是否被成功删除。单ー目的总是易于处理的,事态复杂来源于同时处理几件事情。对于“打开文件”这样的函数而言,一方面我们需要它返回已经打开的文件,一方面我们又需要它报告操作执行的状态ー文件是否被成功打开。当情况变得复杂时,我们可以考虑使用如下几种方案:这是最常使用也是最为简便的ー种方式,如果操作成功,则返回数据,如果失败,则返回nulE这种方式要求返回的数据必须是引用类型(如果是值类型则需要使用可空类型),目.操作结果只有成功或失败两种状态。失败的原因必须显而易见,且往往失败是可以被预见的

106合理的操作结果。例如查找指定姓名的员エ,如果找到则返回员エ信息,找不到则返回nul1。按姓名查找却“找不到”,是正常操作结果中的ー种,并不是错误。6.使用null表示操作失败

1077.使用特别的类型包装数据和状态信息如果操作结果状态多于两种,或者并不适合用“成功”与“失败”来描述时,可以通过ー个特别的类型来提供操作结果,它包含了要返回的数据本身,以及一个状态信息——往往是ー个枚举类型。例如获取ー个指定URL的网页,那么函数除了返回网页内容外,还需要返回状态码ーー无法获取网页内容也许是因为404错误(找不到网页),也许是因为403错误(禁止访问),也许是因为500错误(服务器内部错误)等等。8.抛出异常指示操作失败如果导致操作失败的原因属于意外错误,那么适时抛出异常可能是更好的方式。如果错误的原因不止ー种,应当根据不同的情况抛出不同的异常。关于何时应当返回状态信息,何时应当抛出异常的・般准则,请参阅第15章“异常”的相关内容。9.5.2离开函数的时机离开函数的三种方式中,抛出异常的时机是无法选择的,它关系到函数能否正常执行或者是严垂的安全问题,因此一旦出现问题,必须立即抛出异常。而函数自然退出也没有什么值得讨论的意义。真正能影响代码逻辑的,则是通过return语句进行的强制返回。根据结构化程序设计的要求,函数应该分别只有一个入口和出口——这无疑是否定了在函数结尾处之外的位置使用return的权利。不过在实际开发过程中,单出口的要求并不那么实用。例如下面这个程序:代码示例7T5:使用多出口模式的典型示例publicstaticPhotoFormatFromExtension(stringextension){switch(extension.ToLower())case.bmp":

108returnPhotoFormat.Bmp;case.gif:returnPhotoFormat.Gif;casecase”.jpeg”:returnPhotoFormat.Jpeg;case".png":returnPhotoFormat.Png;case".tif":case.tiri:returnPhotoFormat.Tiff;default:returnPhotoFormat.Unknown;}}这个程序完全违背了单出口的设计原则,但它仍然非常直观,逻辑清晰可见。阅读程序时,一眼可以看出这个并列关系,ー个分支读完,就知道程序执行完毕,无须再阅读后面的部分。相反,如果将其改为单出口模式,我们还必须再引入ー个临时的本地变量:publicstaticPhotoFormatFromExtension(stringextension)|PhotoFormatresult=null;switch(extension.ToLower()){case".bmp":result=PhotoFormat.Bmp;break;case.gir:result=PhotoFormat.Gif;

109break;casejpg”:case”.jpeg”:result=PhotoFormat.Jpeg;break;casepng":result=PhotoFormat.Png;break;case".tif":case.tiff:result=PhotoFormat.Tiff;break;default:result=PhotoFormat.Unknown;break;}returnresult;)这不但增添了许多break语句,读者读完case分支之后,不得不再寻找switch之后的语句继续阅读,因为他们并不知道后面还会发生什么。代码的可读性显然不如前者。不过,如果分支本身并不能解决所有问题的话,或者并不是每个分支都可以独立完成任务时,下面的单出口模式则更为严谨:代码示例7-16;采用单出口模式的典型示例publicQuestionAddNew(stringtypeCode)(Questionnewq=null;

110switch(typeCode)

111caseQuestionTypes.Text:newq=newTextQuestion(ownerQuestionnaire);break;caseQuestionTypes.RadioChoices:newq=newRadioChoicesQuestion(ownerQuestionnaire);break;caseQuestionTypes.CheckChoices:newq=newCheckChoicesQuestion(ownerQuestionnaire);break;caseQuestionTypes.RadioTableChoices:newq=newRadioTableChoicesQuestion(ownerQuestionnaire);break;default:thrownewInvalidCastException("未知的问题类型标识:"+typeCode);)Add(newq);returnnewq;)由于switch中每个分支并不能完成所有任务,它们在给出ー个中间结果后,还需要进ー步的处理。在这个程序中,处理只是简单地调用Add函数,似乎直接写进case分支也不会增加太多的复杂度。但如果这个处理涉及到更多的操作时,分散在case分支内就会导致代码

112冗余。应该让switch结构专心产生一个中间结果,然后再让后面的代码继续工作,最后从ー个出口离开函数。[1]屏幕截图的使用已得到Microsoft公司的许可。[2]屏幕截图的使用已得到Microsoft公司的许可。[3I关于如何合理划分程序集的问题,涉及到程序的结构设计,超出了本章的范围,我们将在后面的章节讨论。[4]“可被公开访问”包括public和internal两种情况,由于对于其单个程序集来说,public和internal的访问权限是一致的,都可以被其他类型访问,因此通常我们不严格区分它们。本书中如果未经特别说明,“公开的"ー词同时表示public和internal。[5]屏幕截图的使用已得到Microsoft公司的许可。[6]A.S.Tanenbaum,andA.S.Woodhull:OperatingSystems:DesignandImplementation(2ndedition).NJ:PrenticeHal1,pp.523-903,1997.[7]屏幕截图的使用已得到Microsoft公司的许可。[8]屏幕截图的使用已得到Microsoft公司的许可。[9]屏幕截图的使用已得到Microsoft公司的许可。[10I屏幕截图的使用已得到Microsoft公司的许可。[11]在.NET2.0中的实际测量值为〇。事实上,MSDN也没有对这个默认初始容量给出确定的值,根据微软惯常的说法,此默认值可能会在后续版本中被更改,因此不应依据该值进行任何假设。也许是为了尽可能地做到向前向后兼容,该值没有被写入文档。[12]屏幕截图的使用已得到Microsoft公司的许可。[13]本书中的术语使用在不引起混淆的前提下尽可能地与MSDN简体中文语言版本保持一致。“overload”被称为“重载”,而“override”被称为“重写”。

当前文档最多预览五页,下载文档查看全文

此文档下载收益归作者所有

当前文档最多预览五页,下载文档查看全文
温馨提示:
1. 部分包含数学公式或PPT动画的文件,查看预览时可能会显示错乱或异常,文件下载后无此问题,请放心下载。
2. 本文档由用户上传,版权归属用户,天天文库负责整理代发布。如果您对本文档版权有争议请及时联系客服。
3. 下载前请仔细阅读文档内容,确认文档内容符合您的需求后进行下载,若出现内容与标题不符可向本站投诉处理。
4. 下载文档时可能由于网络波动等原因无法下载或下载错误,付费完成后未能成功下载的用户请联系客服处理。
最近更新
更多
大家都在看
近期热门
关闭