《一站式示例代码编程规范》由会员上传分享,免费在线阅读,更多相关内容在行业资料-天天文库。
本文档描述了微软ー站式代码示例库项目组所采纳的关于本地C++和.NET(C#和VB.NET)代码的编程风格指导规范。
1鸣谢本文档的每・章节都应该感谢DanRuder•微软PrincipalEscalationEngineer。Dan对本文档进行了斟字酌句的查阅,并根据其20余年的编程经验提供了大量的珍贵评论。我很荣幸能与他共事。我同样感谢微软的四位经理,感谢他们对该项目的ー贯支持。他们是VivianLuo,AllenDing,FelixWu和MeiLiang,同时,如果没有如下ー站式代码示例库项目的关键成员的辛勤付出,本文档必定不会具有现在的技术深度以及完整度,我在这里要感谢他们:HongyeSunJieWangJiZhouMichaelSunKiraQianLindaLiuAllenChenYi-LunLuoStevenChengWen-JunZhang本文档部分章节参考自ー些微软产品组的编程规范。感谢他们的慷慨共享。本编程规范在不断改善。如果您发现某些最佳实践或者话题并没有涵盖在本文档中,请告知我们ー站式示例代码库项目组,以不断充实改善本文档。我期待着您的参与。@声明本编程规范文档以“如是”提供,无论明示或暗示都不包含任何形式保证,但并不限制适用于特殊目的的默认担保。当您编写VC++/VC#/VB.NET代码时,敬请参考或使用本文档。但是,我们希望您能通过onecode@microsoft.com告知我们您正在使用本文档,或给出任何改进建议。
2目录1概览11.1原则和主旨11.2术语22通用编程规范1.1明确和一致31.2格式和风格31.3库的使用41.4全局变量41.5变量的声明和初始化51.6函数的声明和调用51.7语句61.8枚举71.9空格101.10大括号111.11注释111.12代码块183C++编程规范193.1编译器选项193.2文件和结构203.3命名规范213.4指针233.5常量233.6类型转换243.7Sizeof243.8字符串253.9数组263.10宏263.11函数273.12结构体293.13类错误!未定义书签。3.14COM343.15动态分配35
33.1错误和异常353.2资源清理373.3控制流384.NET编码规范414.1类库开发设计规范414.2文件和结构414.3程序集属性414.4命名规范414.5常量434.6字符串434.7数组和集合444.8结构体464.9类474.10命名空间504.11错误和异常504.12资源清理524.13交互操作61
41概览本文档为ー站式示例代码库项目组所使用的C++以及.NET编码规范。该规范源自于产品开发过程中的经验,并在不断完善。如果您发现ー些最佳实践或者话题并没有涵盖在本文档中,请联系我们ー站式示例代码库项冃组,以不断充实完善本文档。任何指导准则都可能会众口难调。本规范的目的在于帮助社区开发者提高开发效率,减少代码中可能出现的bug,并增强代码的可维护性。万事开头难,采纳ー个不熟悉的规范可能在初期会有一些棘手和困扰,但是这些不适应很快便会消失,它所带来的好处和优势很快便会显现,特别是在当您接手他人代码时。1.1原则和主旨高质量的代码示例往往具有如下特质:1.易懂一代码示例必须易读且简单明确。它们必须能展示出重点所在。示例代码的相关部分应当易于重用。示例代码不可包含多余代码。它们必须带有相应文档说明。2.正确性一示例代码必须正确展示出其欲告知使用者的重点。代码必须经过测试,且可以按照文档描述进行编译和运行。3.一致性-示例代码应该按照一致的编程风格和设计来保证代码易读。同样的,不同代码示例之间也应当保持一致的风格和设计,使使用者能够很轻松的结合使用它们。一致性将我们ー站式示例代码库优良的品质形象传递给使用者,展示出我们对于细节的追求。4.流行性-代码示例应当展示现行的编程实践,例如使用Unicode,错误处理,防御式编程以及可移植性。示例代码应当使用当前推荐的运行时库和API函数,以及推荐的项目和生成设置。5.可靠性-代码示例必须符合法律,隐私和政策标准和规范。不允许展示入侵性或低质的编程实践,不允许永久改变机器状态。所有的安装和执行过程必须可以被撤销。6.安全性-示例代码应该展示如何使用安全的编程实践:例如最低权限原则,使用运
5行时库函数的安全版本,以及SDL推荐的项目设置。合理使用编程实践,设计和语言特性决定了示例代码是否可以很好满足上述特性。本编程规范致カ于帮助您创建代码示例以使使用者能够作为最佳实践来效仿和学习。1.2术语在整个文档中,会有一些对于标准和实践的推荐和建议。ー些实践是非常重要的,必须严格执行,另ー些指导准则并不一定处处适用,但是会在特定的场景下带来益处。为了清楚陈述规范和实践的意图,我们会使用如下术语。术语意图理由0一定请...该规范或实践在任何情况卜.都应该遵守。如果您认为您的应用是例タト,则可能不适用。该规范用于减少bug。図一定不要…不允许应用该规范或实践。0您应该…该规范和实践适用于大多数情况。该规范用于统ー编程风格,保持一致和清図您不应该“不应该应用该规范或实践,除非有合理的理由。晰的风格。63您可以…该标准和规范您可以按需应用。该规范可用于编程风格,但不总是有益的。
6OptionsFileExtension>AllLanguages>Basic>C>,C/C++GeneralTabsFormatting>CSS>HTML>JScript>PL/SQL>PlainText>SQLScript>T-SQL>T-SQL7>T-SQL80>T-SQL90r>XAMLVisualC++sample://Getanddisplaywhethertheprimaryaccesstokenof//belongstouseraccountthatisamemberoftheloca//groupevenifitcurrentlyisnotelevated(IsUserlnHWNDhlnAdminGroupLabel=GetDlgltem(hWnd,IDCINADMINGtry(BOOLconstflnAdminGroup=IsUserlnAdminGroupO;SetWindowText(hlnAdminGroupLabel,flnAdminGroup?L)catch(DWORDdwError)(SetWindowText(hlnAdminGroupLabel,L"N/AW);ReportError(L"IsUserInAdminGroup",dwError);}VisualC#示例:2通用编程规范这些通用编程规范适用于所有语言ー它们对代码风格,格式和结构提供了全局通用的指导。2.1明确性和一致性团一定请确保代码的明确性,易读性和透明性。编程规范致カ于确保代码是易懂和易维护的。没有什么胜于清晰、简洁、自描述的代码。0一定请确保一旦应用了某编程规范,需在所有代码中应用,以保持一致性。2.2格式和风格図一定不要使用制表符。不同的文字编辑器使用不同的空格来生成制表符,这就带来了格式混乱。所有代码都应该使用4个空格来表示缩进。可以配置VisualStudio文字编辑器,以空格代替制表符。IndentingC'NoneBlockQSmartTabTabsize4Indentsize4修!nsertspacesC'Keeptabs0您应该限制一行代码的最大长度。过长的代码降低了代码易读性。为了提高易读性,将代码长度设置为78列。若78列太窄,可以设置为86或者90。
7//Getanddisplaywhethertheprimaryaccesst//touseraccountthatisamemberoftheloca//ifitcurrentlyisnotelevated(IsUserlnAdztry(boolflnAdminGroup=IsUserlnAdminGroupO;this.IblnAdminGroup.Text=flnAdminGroup.To}catch(Exceptionex)(this.IblnAdminGroup.Text="N/AW;Me33ageBox.Show(ex.Message,"AnerroroccurMessageBoxButtons.MessageBoxIcon.BxVisualBasicsample:,Getanddisplaywhethertheprimaryaccesstc,touseraccountthatisamemberofthelocal,ifitcurrentlyisnotelevated(IsUserlnAdmiTryOptions,EnvironmentGeneralAdd-in/MacrosSecurityAutoRecoverDocumentsFindandReplaceFontsandColors•HelpImportandExportSettingsInternationalSettingsKeyboardStartupTaskListWebBrowserPerformanceToolsDProjectsandSolutions■SourceControl■TextEditorGeneralShowsettingsfor[TextEditorFont(boldtypeindicatesfixed-widthfonts):CourierNewDisplayitems:ItPlainTextSelectedTextInactiveSelectedTextIndicatorMarginLineNumbersVisibleWhiteSpaceBookmarkBraceMatching(Highlight)BraceMatching(Rectangle)Breakpoint(Disabled)Breakpoint(Enabled)Breakpoint(Error)4IriiDimflnAdminGroupAsBoolean=Me.IsUserlnAdminGroupMe.IblnAdminGroup.TextCatchexAsExceptionflnAdminGroup.ToStスすg库的使用Me.IblnAdminGroup.Text="N/A”MessageBox.Show(ex.Message,"Anerroroccur異a-定微風%2蝴階,,包括不必要的头文MessageBoxButtons,OK,MessageBoxIcon.Error)ddTry件,或引用不必要的程序集。注重细节能够减少项目生成时间,最小化出错几率,并给读者ー个团一定请在您的代码编辑器中使用定宽字体,例良好的印象。如CourierNew〇2.4全局变量図一定请尽量少用全局变量。为了正确的使用全局变量,一般是将它们作为参数传入函数。永远不要在函数或类内部直接引用全局变量,因为这会引起一个副作用:在调用者不知情的情况下改变了全局变量的状态。这对于静态变量同样适用。如果您需要修改全局变量,您应该将其作为一个输出参数,或返回其ー份全局变量的拷贝。
82.5变量的声明和初始化回一定请在最小的,包含该局部变量的作用域块内声明它。一般,如果语言允许,就仅在使用前声明它们,否则就在作用域块的顶端声明。□ー定请在声明变量时初始化它们。团一定请在语言允许的情况下,将局部变量的声明和初始化或赋值置于同一行代码内。这减少了代码的垂直空间,确保了变量不会处在未初始化的状态。//C++sample:HANDLEhToken=NULL;PSIDplntegritySid=NULL;STARTUPINFOsi={sizeof(si)};PROCESS_INFORMATIONpi={0};//C#sample:stringname=myObject.Name;intval=time.Hours;•VB.NETsample:DimnameAsString=myObject.NameDimvalAsInteger=time.Hours図一定不要在同一行中声明多个变量。推荐每行只包含一一句声明,这样有利于添加注释,也减少歧义。例如VisualC++示例,Good:CodeExample*pFirst=NULL;//Pointerofthefirstelement.CodeExample*pSecond=NULL;//Pointerofthesecondelement.Bad:CodeExample*pFirst,*pSecond;后ー个代码示例经常被误写为:CodeExample*pFirst,pSecond;这种误写实际上等同于:CodeExample*pFirst;CodeExamplepSecond;2.6函数的声明和调用函数或方法的名称,返回值,参数列表可以有多种形式。原则上应该都将这些置于同一行代码内。如果带有过多参数不能置于一行代码,可以进行换行:多个参数一行或者ー个参数一行。将返回值置于函数或方法名称的同一行。例如,单行格式://C++functiondeclarationsample:HRESULTDoSomeFunctionCa11(intparamlintparam2,int*param3);//C++/C#functioncallsample:hr=DoSomeFunctionCall(paraml,param2,param3);'VB.NETfunctioncallsample:hr=DoSomeFunctionCall(paraml,param2,param3)多行格式://C++functiondeclarationsample:HRESULTDoSomeFunctionCa11(intparamlintparam2,int*param3,intparam4,intparam5);//C++/C#functioncallsample:hr=DoSomeFunctionCall(paraml,param2,param3,param4,param5);'VB.NETfunctioncallsample:hr=DoSomeFunctionCa11(paraml,param2,param3,_param4,param5)将参数列表置于多行代码时,每ー个参数应该整
9齐排列于前ー个参数的下方。第一个类型/参数对置于新行行首,并缩进ー个制表符宽度。函数或方法调用时的参数列表同样需按照这一格式。//C++sample:HRESULTDoSomeFunctionCa11(HWNDhwnd,//Youcancommentparameters,tooT1paraml,//IndicatessomethingT2param2,//IndicatessomethingelseT3param3,//IndicatesmoreT4param4,//IndicatesevenmoreT5param5);//YougettheideaIIC++/C#samplehr=DoSomeFunctionCa11(hwnd,paraml,param2,param3,param4,param5);'VB.NETsample:hr=DoSomeFunctionCall(hwnd,paraml,_param2,_param3,param4,_param5)团一定请将参数排序,并首先将输入参数分组,再将输出参数放置最后。在参数组内,按照能够帮助程序员输入正确值的原则来将参数排序。比如,如果ー个函数带有2个参数,"left"和“right",将"left"置于"right”之前,则它们的放置顺序符合其参数名。当设计ー系列具有相同参数的函数时,在各函数内使用一致的顺序。比如,如果ー个函数带有一个输入类型为句柄的参数作为第一参数,那么所有相关函数都应该将该输入句柄作为第一参数。2.7代码语句図一定不要在同一行内放置一句以上的代码语句。这会使得调试器的单步调试变得更为困难。Good://C++/C#sample:a=1jb=2;'VB.NETsample:If(IsAdministrator())ThenConsole.WriteLine(••YES'1)EndIfBad://C++/C#sample:a=1;b=2;'VB.NETsample:If(IsAdministrator())ThenConsole.WriteLine("YES")
102.8枚举回一定请将代表某些值集合的强类型参数,属性和返回值声明为枚举类型。团一定请在合适的情况下尽量使用枚举类型,而不是静态常量或"#define"值.枚举类型是一个具有一个静态常量集合的结构体。如果遵守这些规范,定义枚举类型,而不是带有静态常量的结构体,您便会得到额外的编译器和反射支持。Good://C++sample:enumColor{Red,Green,Blue);//C#sample:publicenumColor(Red,Green,Blue)1VB.NETsample:PublicEnumColorRedGreenBlueEndEnumBad://C++sample:constintRED=0;constintGREEN=1;constintBLUE=2;#defineRED#defineGREEN#defineBLUEclassColor//C#sample:publicstaticpublicconstintRed=0;publicconstintGreen=1publicconstintBlue=2;'VB.NETsample:PublicClassColorPublicConstRedAsInteger=0PublicConstGreenAsInteger=1PublicConstBlueAsInteger=2EndClass図一定不要使用公开集合作为枚举(例如操作系统版本,您亲朋的姓名)。团一定请为简单枚举提供ー个0值枚举量,可以考虑将之命名为"None"。如果这个名称对于特定的枚举并不合适,可以自行定义为更准确的名称。//C++sample:enumCompression(None=0,GZip,Deflate);//C#sample:publicenumCompression(None=0,GZip,
11DeflateThrowNew*VB.NETsample:PublicEnumCompressionNone=0GZipDeflateEndEnum図一定不要在.NET中使用Enum.lsDefined来检查枚举范围。Enum.lsDefined有2个问题。首先,它加载反射和大量类型元数据,代价极其昂贵。第二,它存在版本的问题。Good://C#sample:if(c>Color.Black||c
12示例。然而,如果ー个标志枚举中的某些值组合起来是非法的,您就不应该创建这样的标志枚举。//C++sample:enumFileAccess(Read=0x1,Write=0x2,ReadWrite=Read|Write);//C#sample:[Flags]publicenumFileAccess(Read=0x1,Write=0x2,ReadWrite=Read|Write)'VB.NETsample:
132.9空格□您应该使用2行空行来分隔方法实现或类型声明。2.9.I空行図您应该使用空行来分隔相关语句块。省略额外2.9.2空格的空行会加大代码阅读难度。比如,您可以在变空格通过降低代码密度以增加可读性。以下是使量声明和代码之间有一行空行。用空格符的一些指导规范:Good://C++sample:0您应该像如下般在一行代码中使用空格。voidProcessitem(constItem&item)(Good:intcounter=0;//C++/C#sample:CreateFoo();//Noif(...){})spacebetweenfunctionnameandparenthesisMethod(myChar,0,1);//Singlespaceafteracommax=array[index];//NoBad://C++sample:spacesinsidebracketswhile(x==y)//SinglevoidProcessitem(constItem&item)(intcounter=0;spacebeforeflowcontrolstatementsif(x==y)//Singlespaceseparatesoperators•VB.NETsample://Implementationstartshere//if(...){)CreateFoo()'NospacebetweenfunctionnameandparenthesisMethod(myChar,0,1),Singlespaceafteracommax=array(index)'Nospacesinsidebrackets)While(x=y)*Singlespacebeforeflowcontrolstatements在本例中,过多的空行造成了空行滥用,并不能If(x=y)Then*Singlespaceseparatesoperators使代码更易于阅读。Bad://C++/C#sample:CreateFoo();//Spacebetweenfunctionnameandparenthesis
14Method(myChar,0,1);//NospacesaftercommasCreateFoo(myChar,0,1);//Spacebeforefirstarg,afterlastargx=array[index];//Spacesinsidebracketswhile(x==y)//Nospacebeforeflowcontrolstatementsif(x==y)//Nospaceseparatesoperators•VB.NETsample:CreateFoo(),SpacebetweenfunctionnameandparenthesisMethod(myChar,0,1)*NospacesaftercommasCreateFoo(myChar,0,1),Spacebeforefirstarg,afterlastargx=array(index)'SpacesinsidebracketsWhile(x=y)•NospacebeforeflowcontrolstatementsIf(x=y)Then•Nospaceseparatesoperators2.10大括号0一定请在一站式示例代码库的代码示例中使用Allman风格的大括号用法。Allman风格是以EricAllman命名的,有时也被称为"ANSI风格"。该风格将大括号与相关代码置于下一行内,与控制语句的缩进相同。大括号内的语句缩进ー个等级。Good://C++/C#sample:if(x>5)(y=o;)1VB.NETsample:If(x>5)Theny=0EndIfBad(inAll-In-OneCodeFrameworksamples)://C++/C#sample:if(x>5){y=0;}0您应该在即使是单行条件式的情况下也使用大括号。这样做使得将来增加条件式更简便,并减少制表符引起的歧义。Good://C++/C#sample:if(x>5)(y=o;)•VB.NETsample:If(x>5)Theny=0EndIfBad://C++/C#sample:if(x>5)y=0;•VB.NETsample:If(x>5)Theny=02.11注释0您应该使用注释来解释一段代码的设计意图。一定不要让注释仅仅是重复代码。
15Good://DeterminewhethersystemisrunningWindowsVistaorlateroperating//systems(majorversion>=6)becausetheysupportlinkedtokens,but//previousversions(majorversion<6)donot.If(Environment.OSVersion.Version.Major>=6)ThenEndIf0您应该将注释缩进为被其描述的代码的同一级。Bad://Thefollowingcodesetsthevariableitothestartingvalueofthe//array.Thenitloopsthrougheachiteminthearray.0您应该使用‘〃‘注释方式,而不是7**/,来为C++和c#代码作注释。即使注释跨越多行代码,单行注释语法(〃.•仍然是首选。0您应该在注释中使用首字母大写的完整的句子,以及适当的标点符号和拼写。Good://IntializethecomponentsontheWindowsForm.InitializeComponent();'IntializethecomponentsontheWindowsForm.//DeterminewhethersystemisrunningWindowsVistaorlateroperating//systems(majorversion>=6)becausetheysupportlinkedtokens,but//previousversions(majorversion<6)donot.if(Environment.OSVersion.Version.Major>=6)()'Getanddisplaytheprocesselevationinformation(IsProcessElevated)1andintegritylevel(GetProcessIntegrityLevel).Theinformationisnot,availableonoperatingsystemspriortoWindowsVista.Bad://intializethecomponentsontheWindowsForm.InitializeComponent();,intializethecomponentsontheWindowsFormInitializeComponent()2.11.1内联代码注释内联注释应该置于独立行,并与所描述的代码具有相同的缩进。其之前需放置ー个空行。其之后不需要空行。描述代码块的注释应该置于独立行,与所描述的代码具有相同的缩进。其之前和之后都有一个空行。举例:if(MAXVAL>=exampleLength)InitializeComponent()//Reprorttheerror.ReportError(GetLastError());//Thevalueisoutofrange,wecannotcontinue.returnE_INVALIDARG;)当内联注释只为结构体,类成员变量,参数和短语句做描述
16时,则内联注释允许出现在和实际代码的同一行。在本例中,最好将所有变量的注释对齐。classExample(public:voidTestFunction(do(}while(!fFinished);//Continueifnotfinished.}private:intm_length;//Thelengthoftheexamplefloatm_accuracy;//Theaccuracyoftheexample図您不应该在代码中留有过多注释。如果每一行代码都有注释,便会影响到可读性和可理解性。单行注释应该用于代码的行为并不是那么明显易懂的情况。如下代码包含多余注释:Bad://Loopthrougheachiteminthewrinklesarrayfor(inti=0;i<=nLastWrinkle;i++)(Wrinkle*pWrinkle=apWrinkles[i];//Getthenextwrinkleif(pWrinkle->IsNew()&&//Processifit'sanewwrinklenMaxImpact
17Seehttp://www.microsoft.com/opensource/licenses.mspx#Ms-PL.Allotherrightsreserved.THISCODEANDINFORMATIONISPROVIDED••ASIS"WITHOUTWARRANTYOFANYKIND,EITHEREXPRESSEDORIMPLIED,INCLUDINGBUTNOTLIMITEDTOTHEIMPLIEDWARRANTIESOFMERCHANTABILITYAND/ORFITNESSFORAPARTICULARPURPOSE.ヽ***************************************************************************VB.NET文件头注释模板:•★★★★★★★★★★★★★★★★★★★★★★★★★★★★★Modu1eHeader'ModuleName:
182.11.3类注释checktheprivilegelevelofthecurrentprocess,andhowtoself-elevatetheprocessbygivingexplicitconsentwiththeConsentUI.ThissourceissubjecttotheMicrosoftPublicLicense.Seehttp://www.microsoft.com/opensource/licenses.mspx#Ms-PL.Allotherrightsreserved.THISCODEANDINFORMATIONISPROVIDED"ASIS"WITHOUTWARRANTYOFANYKIND,EITHEREXPRESSEDORIMPLIED,INCLUDINGBUTNOTLIMITEDTOTHEIMPLIEDWARRANTIESOFMERCHANTABILITYAND/ORFITNESSFORAPARTICULARPURPOSE.ヽ***************************************************************************III
19/*FUNCTION:
20ThefunctioncheckswhethertheprimaryaccesstokenoftheprocessbelongstouseraccountthatisamemberofthelocalAdministratorsgroup,evenifitcurrentlyisnotelevated.PARAMETERS:hToken-thehandletoanaccesstoken.RETURNVALUE:ReturnsTRUEiftheprimaryaccesstokenoftheprocessbelongstouseraccountthatisamemberofthelocalAdministratorsgroup.ReturnsFALSEifthetokendoesnot.EXCEPTION:Ifthisfunctionfails,itthrowsaC++DWORDexceptionwhichcontainstheWin32errorcodeofthefailure.EXAMPLECALL:try(if(IsUserlnAdminGroup(hToken))wprintf(LnUserisamemberoftheAdministratorsgroup
21M);elsewprintf(LMUserisnotamemberoftheAdministratorsgroup
22M);)catch(DWORDdwError)(wprintf(LMIsUserlnAdminGroupfailedw/err%lu
23",dwError);}III
24,DemoSolution2();2.11.6TODO待办注释図一定不要在已发布的代码示例中使用TODO待办注释。每ー个代码示例都必须完整,在代码中不能有未完成的任务。2.12代码块0一定请在大量代码会因为更具结构化而获益时,使用代码块声明。通过作用域或者功能性分类,将大量代码分组,会改善代码易读性和结构。C++代码块:#pragmaregion"HelperFunctionsforXX"#pragmaendregionCtt代码块:#regionHelperFunctionsforXX#endregionVB.NET代码块:#Region"HelperFunctionsforXX"#EndRegion
25CppWindowsServicePropertyPages▼Platform:|Active(Win32)Create/UsePrecompiledHeaderCreate/UsePCHThroughFilePrecompiledHeaderFileCreate/UscPrecompiledHeaderEnablescreationoruseofaprecompiledhe3C++编程规范以下编程规范适用于本地C++代码。3.1编译器选项3.1.1预编译头図一定不要使用预编译头。VisualC++项目默认使用预编译头文件。其原理是步生成stdafx.h/cpp文件时,巨大的Windows头文件只被编译一次。项目中其他任何ー个.CPP文件都需要首先包含include"stdafx.h",这样项目オ能正确生成。当编译器找到"stdafx.h"时,便知道何时插入预编译头信息。在代码示例中,必须关闭预编译头选项。在您的项目属性中,找到C/C++标签页,选择Precompiledheaders节点。点击Notusingprecompiledheaders单选按钮,之后点击OK。请确保修改了所有的配置(包括Debug配置和Release配置)。之后,移除所有源文件的#include
26^ConfigurationProperties","C/C++”,"General”,将“WarningLevel"设置为"Level4”。3.2文件和结构3.2.1stdafx.h,stdafx.cpp,targetver.h0您应该删除VisualStudio项冃模板生成的stdafx.h,stdafx.cpp和targetver.h文件以保持示例的简洁。然而,如果您有许多被大量代码文件共享的标准头文件,您可以创建单独的文件来包含它们。这非常类似于Windows.h的作用。3.2.2头文件团一定请在头文件内使用包含保护符(includeguards),来防止头文件被无意的多次包含。以下示例代码中的#ifndef和#endif,应该为头文件的第一行和最后一行代码。示例代码展示了如何在"CodeExample.h"中使用"#ifndef/#endiア作为包含保护符。//Fileheadercommentgoesfirst...#ifndefCODE_EXAMPLE_H_#defineCODE_EXAMPLE_H_classCodeExample();#endif您也可以使用“#pragmaonce"(微软编译器的ー个特定拓展)来替代"#ifndef/#endir包含保护符://Fileheadercommentgoesfirst...#pragmaonceclassCodeExample();図您不应该在头文件内实现函数。头文件只能包含函数声明和数据结构。它们的实现应置于.cpp文件。3.2.3实现文件实现文件包含了全局函数,局部函数和类方法实际的函数体。实现文件是拓展名为(或者.cpp的文件。注意,实现文件不必包含整个模块的完整实现。它可以被分隔,并包含ー个公共内部接口。团您应该将不必导出的声明放置在实现文件中。此外,您应该为它们加上static关键字,以限制其作用域在该.cpp/.c文件定义的编译单元。这将减少当链接2个或更多使用了相同内部变量的.cpp
27函数,方法Pascal规范名词或或动名词voidPrvoidPr接口PascalC规范,T前缀带名字interfa结构体所有字母大写,分隔单词以名词struct宏,常量所有字母大写,ご分隔单词以#define#define#defineconsti参数,变量Camel规范名词example模板参数Pasca!规范,T前缀带名词T,TIte3.3.3匈牙利命名法文件时,出现“multiply-definedsymbol”错误的情况。3.2命名规范3.2.1通用命名规范团一定请为各种类型,函数,变量,特性和数据结构选取有意义的命名。其命名应能反映其作单字符变量应该仅用于计数器(i,j)或者坐标(x,y,z)〇根据经验,变量作用域越大,便越应该使用描述性强的命名。図您不应该在标识符名中使用缩短或缩略形式的词。比如,使用“GetWindow”而不是"GetWin”。对于公共类型,线程过程,窗口过程,和对话框図您可以对参数和变量使用匈牙利命名法。然而,匈牙利命名法相当老旧,会对代码重构造成困难。例如,当改变变量类型时,您需要对所有代码都进行更改。如下表格定义了一套配套的匈牙利命名法标记,如果您使用匈牙利命名法,建议使用它们。过程函数,为〃ThreadProc","DialogProc”,"WndProc”等使用公共后缀。类型!标记描述boolzBOOL,bitfieldfー个旗标。例如,BOOLfSucceeded3.3.2标识符的大小写命名规范如下表格描述了对不同类型标识符的大小写命名规范。BYTEー个8位无符号数cBYTE应当仅用于不iWORD/ー个16位无符号数。WORD应当仅用于DWORDdwー个32位无符号数。DWORD应当仅用]HRESULThrHRESULT一般用了Win32的错谡或VARIANTvt个OLEVARIANT^,HANDLEhー个句柄int.ー个32位序数(可以用于<.<=,>,>=等ヒ标识符1大小写命名规范命名结构疆signedint/位.类Pascal规范名词cl^gComplexNumber{classCodeExample{..ci般aw!碩。rfet{...ー个16位序数。尽量不m图使用这些标志枚举Pascal规范各字enfcjnf/pe{...};ー个32位序数。尽量不要使用这哨标志
28unsignedlong一3.3.4用户界面控件命名规范_Jnt64,LONGLONG,ULONGLONG/ー个64位序数。用户界向榨件团•仲用如下前绛.以及陥后的咨源TCHAR,wchar__t,charch•个字符(未指明符号)。"wchajビ类型更适合于宽字符,因为其属于C++内置类型。对于char髏然ポ黑辘體験猿體黨紋嬲瓢喪码嬲’噺崔WPWSTR,PCWSTR,pszー个指向〇值结尾的字符串。因为我们在项目中都使用Unicode,我们不必为PSTR和PWSTR设置wchar_t*,PSTR,PCSTR,char*不同标志,对于char和TCHARSpsz”代表PSTR和"pwsz"代表和PSTR。所有指针都是长指针控件类型控件句柄命名格式L1FC且wchar_t[],char[]sz,个〇值结尾的字符小(形式为翻,跖喇卿,斜]叮%如,wcharszMessage[BUFFER_SIZE]PuttonpXxxAnimatehXxxButtonabniXxxtnXxxBSTRbstrー个OLE白动化的BSTR.CheckBoxhXxxCheckcikXxxvoid一ー个void〇对于指向void的指针前缀hXxxCombocmbXxx(*)()ー个函数。对于指向函数的指针Q雌TJW'eijpfckerhXxxDatePickerdtpXxxEditControlhXxxEdittbXxx举例,GroupBoxhXxxGroupgrpXxxHorizontalScrollBarhXxxScrollhsbXxxHANDLEhMapFile=NULL;IPAddressControlhXxxIpAddripXxxDWORDdwError=N0_ERR0R;ListBoxhXxxListIstXxx匈牙利命名法的前缀用于增加类型信息ー以下是匈List(View)ControlhXxxListIvwXxxMenuhXxxMenuN/A牙利命名标志的前缀:MonthCalendarControlhXxxCalendarmclXxxPictureBoxhXxxPicturepctXxxPrefixDescriptioiProgressControlhXxxProgressprgXxxPー个指针(32位或64位,取决于平台).RadinRnvhXxxRadior;ョdXxxー个‘智能’指针,例如,ー个拥有类指针语义电类SPc数量。比如,cbBuffer代表缓冲(buffer)的骑喊"c卜牌蹴ド志也是可以接ナ.f|XxxRicnEciitと的ctfXxxm_类中的个成员变量SliderControlhXxxSIidersdXxxs_类中的一个静态成员变量SpinControlhXxxSpinSonXxxg.ー个全局变量StaticTexthXxxLabelIt)Xxx1COM接口SysLinkControlhXxxLinkIrikXxx举例,TabControlhXxxTabtabXxxTree(View)ControlhXxxTreetvwXxxUINTcch;//CountofcharactersVerticalScrollBarhXxxScrollvsbXxxPWSTRpsz;//Stringpointer,nullterminatedwchar_tszString[]=L'*fooH;
293.2指针回您应该总是在声明指针时进行初始化,释放之后应赋予NULL值或其他无效值。此举防止了其余代码使用未初始化的指针,意外的读写未知地址而破坏进程地址空间。举例:Good:BINARY_TREE*directoryTree=NULL;DWORD*pdw=(DWORD*)LocalAlloc(LPTR,512);if(pdw!=NULL)(LocalFree(pdw);pdw=NULL;)if(directoryTree!=NULL)(//FreedirectoryTreewithmatchtowayitwasallocatedFreeBinaryTree(directoryTree);directoryTree=NULL;)0您应该在指定一个指针类型/变量时,在‘*‘字符和类型之间留有一个空格,而不要在'*'字符和变量之间留有空格。该规范使得代码更一致。以下是示例:Good:HRESULTGetlnterface(IStdlnterface**ppSI);INFO*GetInfo(DWORD*pdwCount);DWORD*pdw=(DWORD*)pv;IUnknown*pUknwn=static_cast
30constintSunday=0;constintMonday=1;constintBlack=0;constintBlue=1;//Notetheweaktypeparameterchecking;callingcodecanreversethem.BOOLColorizeCalendar(inttoday,inttodaysColor);位平台下,它不能通过编译。记得对您reinterpret_cast<>的用法进行注释,这有助于减少读者在看到此类型转换时的担心。3.const_cast用于移除对象的‘const’性质。以上三种转换的语法是类似的:叼您应该在适当的时候,对传入和返回参数加上'const,修饰符。通过应用'const'修饰符便清楚表明了该参数的意图,编译器能增加一层额外的验DerivedClass*pDerived=HelperFunction();BaseClass*pBase=staticcast
31使用宽字符类型,包括wchar_t,PWSTR,PCWSTR,不要使用TCHAR版本的类型。Good:HRESULTFunction(PCWSTR)Bad:HRESULTFunction(PCTSTR)变量名不应该指明宽字符的〃W"。Good:Function(PCWSTRpsz)Bad:Function(PCWSTRpwsz)不要使用TEXT宏,而使用L前缀来创建Unicode字符串常量L"字符串值".Good:L"fooHBad:TEXT("foo")相较WCHAR,倾向于选择wchart_t,因为其是原生的C++类型。Good:L'^foo"wchar_tszMessage[260];Bad:TEXT(“foo”)WCHARszMessage[260];永远不要显式的使用A/W版本的API。这是低劣的编程风格,因为API的名称原本便是没有A/W的。同样的,硬编码会对在ANSI/Unicode之间的移植增加难度。Good:CreateWindow(...);Bad:CreateWindowW(...);团您应该在条件允许的情况下,为字符串使用固定大小栈缓冲区,而不是动态分配。使用固定大小栈缓冲区有如下好处:•更少的错误状态,无需检验分配失败,无需编写代码来处理这些情况的发生。•绝不会出现内存泄露,因为栈会帮您自动回收内存。•更好的性能表现,不会出现临时堆的使用。以下几种情况,应该避免使用栈缓冲区:•它不适用于当ー些数据量是任意的情况。注意:用户界面字符串受到UA指导准则和屏幕尺寸的限制,所以您一般可以为其大小
32设定一个上限。将您所认为的字符串大小翻倍,以适应将来在其他语言中的增长(据统计,一般有30%的语言增长。)•“大数据";大小超过MAX_PATH(260)几倍,或者超过MAX_URL(2048),这些情况下便不适用。•递归函数所以对于取了合理最大值的小数据,请尽量将数据置于栈上。3.9数组3.9.1数组大小0一定请尽量使用ARRAYSIZE()来获得数组大小。当用于非数组类型时,ARRAYSIZE。会产生・个错误:错误C2784。对于匿名类型,您需要使用更低安全性的一ARRAYSIZEO宏。使用ARRAYSIZE()来代替RTL_NUMBER_OF(),_countof(),NUMBER_OF())等等。团一定请从数组变量取得其大小,而不要在代码中指定其大小:Good:ITEMrgltems[MAX_ITEMS];for(inti=0;i 33将最后错误代码(lasterror)设置为ERRORJNVALID一PARAMETER,并返回HRESULTE_INVALIDARGo3.9.1引用参数図一定不要使用引用参数作为输出参数。因为很难得知在函数调用处,变量是否被修改了。此时应该使用指针。比如,考虑如下函数:Function()(intn=3;Mystery(n);ASSERT(n==3);)该断言是否有效?如果您并不知道函数的声明,您可能会想:"当然了,n的值肯定不会被修改"。然而,Mystery函数声明如下:voidMystery(intin);那么答案便是"可能被修改,也可能没有"。如果Mystery函数有修改其实参的意图,它应该被重写为:voidMystery(int*pn);Function()(intn=3;Mystery(&n);}现在,我们便知道Mystery函数可能会修改其实参。如果您选择按引用传递对象(比如,ー个结构体),您可以选择显式的通过指针来传递(如果其是ー个输出参数),或者使用const引用(如果其是ー个输入参数)。const属性指明函数不应修改该对象。这遵守了"不带&的传入参数不会被函数修改"的规范。我们已经为对象类型的常见情景定义了一些宏,例如REFCLSID,REFIID和REFPROPERTYKEYo3.9.2未引用参数当实现接口或标准导出内的方法时,有一些参数没有被引用是相当常见的。编译器会发现未使用的参数,并产生一个警告,有些组件甚至会认为这是ー个错误。为避免发生如此情况,将未使用的参数使用/・参数名ッ语法将其注释掉。不要使用UNREFERENCED一PARAMETER()宏,因为其1)太繁琐,2)并不能保证参数实际上真的未被引用。Good:LRESULTWndProc(HWNDhwnd,UINTuMsg,WPARAMwParam,LPARAM/*1Param*/)Bad:LRESULTWndProc(HWNDhwnd,UINTuMsg,WPARAMwParam,LPARAMiParam)(UNREFERENCED_PARAMETER(IParam);}3.9.3输出字符串参数ー个从函数返回字符串的常见方法是让调用者指定字符串应该存储的地址以及与字符数等长的缓冲区长度。这便是"pszBuf/cchBuf"模式。在方法 34中,您首先需要显式的检验缓冲区大小是否大于0。在COM应用中,您可以返回_outstring字符串参数作为由CoTaskMemAlloc/SysAllocString动态分配的字符串。这便避免了代码中字符串大小的限制。调用者同样负责调用CoTaskMemFree/SysFreeStringo3.9.1返回值团一定请检查函数的返回值,而不是输出参数,以判断该函数是否调用成功。ー些函数会通过多种方式来传递成功或失败状态信息。比如,在COM方法中,在HRESULT和输出参数内便能得到这些信息。如下2例示例都是正确的,但前者更好。Good:IShellltemlmageFactory*psiif;if(SUCCEEDED(psi->QueryInterface(IID_PPV_ARGS(&psiif))))(//Usepsiffpsiif->Release();)Bad:IShellltemlmageFactory*psiif;psi->QueryInterface(IID_PPV_ARGS(&psiif));if(psiif)(//Usepsiffpsiif->Release();理由:•HRESULT具有从函数返回的非常重要的信息,应当对它进行检验。•通常,HRESULT值带有需要被传递的、非常重要的信息,这大大降低了将所有失败映射或认作E_FAIL的机会。•对HRESULT进行检验,使得代码能够与一些错误输出参数并非〇的情况(许多Win32API)保持一致性。«检验HRESULT使得调用和检验置于同一行代码。«检验HRESULT在代码生成方面更高效。 353.9结构体3.9.1Typedef结构体0您应该在创建命名结构体时使用typedef〇如果您想要的只是ー个单结构变量,你便不必这么做。如果需要typedef标签(前置使用和自引用的typedef)1则它应与类型名相同"typedefstructFOO{intx;}FOO;"结构体名应该所有字母大写,并以‘一‘分隔字符。举例://Theformatofthebytesinthedatastream....typedefstructFORM_STREAM_HEADER(intcbTotalSize;intcbVarDataOffset;USHORTusVersion;}FORM_STREAM_HEADER;3.9.2结构体初始化0一定请使用"={}"对结构体置零。PROPVARIANTpv={};当ー个结构体的第一个成员变量是字节大小字段时,您可以使用如下捷径来初始化该字段,并将其余字段置零:SHELLEXECUTEINFOsei={sizeof(sei)};sei.IpFile=...3.9.3结构体vs类0一定请使用结构体来定义ー个不包含函数的数据聚合。如果数据结构包含成员方法,便使用类。对于C++,ー个结构体可以拥有成员函数,操作符以及类中所有的其他特性。实际上,类与结构体差别在于所有结构体内的成员默认访问权限是Public的,但是在类中是Private的。通常情况下,当且仅当拥有成员函数时オ使用类。3.10类3.10.1数据成员図一定不要声明Public数据成员。使用内联访问器函数以提高性能表现。0一定请在构造函数内,倾向使用初始化,而不是赋值操作。初始化示例:Good:classExample(public:private:intm_length;CStringm_description;Example(constintlength,constwchar_t*description):m_length(length),m_description(description),m_accuracy(0.0)floatm_accuracy;};Bad:classExample(public:Example(intlength,constwchar_t♦description)(m_length=length;m_description=description;m_accuracy=0.0;) 36private:intm_length;CStringm_description;floatm_accuracy;);0一定请按照成员变量在类中声明的顺序来初始化它们。初始化的顺序是成员变量在类定义中声明的顺序,而不是它们在初始化列表中的顺序。如果顺序都是一致的,则代码能够反应出编译器生成代码的情况。考虑以下"CodeExample"示例:Bad:classCodeExample{public:explicitCodeExample(intsize);-CodeExample();private:wchar_t*m_buffer;intm_size;);CodeExample::CodeExample(intsize):msize(size),m_buffer((wchar_t*)operatornew[](m_size))()CodeExample::-CodeExample()(delete[]m_buffer;}intwmain(intargc,wchar_t*argv[]){CodeExampleexample(23);return0;}CodeExample类的声明定义了m_buffer和m_size,所以首先初始化m_buffer,接着是m_size〇而构造函数的代码所表示的初始化顺序与声明顺序相反。这会出现当m_buffer在初始化的时候,m_size看起来是有效的,但是事实并非如此。如果改变一下声明顺序,代码便如预期一般运作。3.9.1构造函数团一定请将构造函数的工作量降到最低。除了得到构造函数参数,设置主要数据成员,构造函数不应该有太多的工作量。其余工作量应该被推迟,直到必须。团您应该为类显式的声明复制语义。复制构造函数和赋值操作符是非常特殊的方法一如果您不提供ー个实现,编译器会为您提供ー个默认实现。如果类语义不支持复制,请显式的提供Private的未实现的复制构造函数利赋值操作符来禁用复制语义。举例:classExample(private:Example(constExamplei);Example&operators(constExample&);您不应该提供这些方法的实现,因为当您意外的使用它们时,编译器会报错以警告您。0一定请在定义复制构造函数时,使用‘const,引用类型。比如,对于类T,复制构造函数应该被定义为:T(constT&other){)如果构造函数被定义为"T(T&other)"或者甚至"T(T&other,intvalue=0)”,它们仍然是复制构造函数。通过使用"constT&”进行标准化,构造函数能用于const和非const值,同时拥有const带来的安全性。回一定请以‘explicit,关键字来定义所有单参数构造 37函数,这样,它们便不会成为转换构造函数。举例:classCodeExample(intm_value;public:explicitCodeExample(intvalue)m_value(value)());図一定不要提供转换构造函数,除非类语义需要。3.9.1析构函数0一定请使用析构函数来集中类的资源清理工作(通过delete来释放资源)。如果在析构函数之前,资源被释放,请确保正确设置了该字段,(例如,将指针设置为NULL),以保证析构函数不会重复释放。0一定请在拥有至少ー个虚函数(非析构函数)的类中,将析构函数设置为"virtual"。如果类没有任何虚函数,请不要声明虚析构函数。使用虚析构函数的原则是类有其他的虚函数。假设,类B继承自类A,您有一个类型A的指针p。p实际可以指向A或B。假设A和B有一个虚函数F»若p指向的是A,则p->F()会调用A::F,若P指向的是B,则会调用B::F。同样,您显然需要配对析构函数〜A或〜B,所以析构函数也需是虚函数。如果F不是虚函数会怎样?那么无论指针指向哪个对象,您最终只会调用A::F。如果指针指向B,那您也只能将B当做A。所以,您绝对不会希望调用B::F,因为该成员函数更新了A的状态,而不是B。B的析构函数可能会失败,因为其修改了只适用于B的状态。滥用虚函数会造成大量C++bug。3.9.2操作符図一定不要重载&&,||,„等操作符。和内置的&&,||或,操作符不同,这些重载版本并不能呈现出短路特性,所以使用这些操作符会引起不可意料的结果。図您不应该重载操作符,除非类语义需要。図一定不要在重载时,改变操作符原有语义,不要使用‘+'操作符来做减运算。図您不应该实现转换操作符,除非类语义需要。3.9.3函数重载図一定不要在重载时,随意的变化参数名。如果ー个重载函数中的参数代表着另一个重载函数中相同的参数,该参数则应该有相同的命名。具有相同命名的参数应该在重我函数中出现在同一位置。回一定请仅将最长得重载函数设为虚函数(为了拓展性考虑)。短的重载函数应该调用到长重载函数。3.9.4虚函数□一定请以虚函数来实现多态。 38図一定不要使用虚函数,除非您真的需要。因为虚函数通过虚表调用,会产生额外代价。回您应该在重写虚函数时使用(verride,关键字。(注意:这是微软对于C++的特定拓展)。Override关键字会使编译器确保函数原型与基类中虚函数匹配。如果基类中的虚函数原型稍作修改,编译器便会为需要修改的派生类产生一个错误。举例:classExampleprotected:Example()public:virtual-Example()virtualintMeasureAccuracy();private:Example(constExample&);Examplesoperators(constExample&);};classConcreteExample:publicExample{public:ConcreteExample()-ConcreteExample()(}intMeasureAccuracy()override;);3.9.1抽象类抽象类提供了一个多态基类,需要派生类来提供虚函数实现。図您可以使用‘abstract,关键字来表示抽象类(注意:这是微软对于C++的特定拓展).0•—定请提供ー个Protected构造函数。0一定请将抽象方法设置为纯虚函数。0一定请提供ー个Public的,虚析构函数,如果您允许通过指向抽象类的指针来进行delete操作。若您不想如此,请提供ー个Protected的,非虚析构函数。0一定请显式提供Protected复制构造函数和赋值操作符,或Private未实现的复制构造函数和赋值操作符一如果用户意外的使用了抽象基类进行一个传值操作,便会产生一个编译错误。抽象类示例:classExampleabstract(protected:Example()(}public:virtual-Example() 39{)virtualintMeasureAccuracy()=〇;private:Example(constExample&);Example&operator=(constExample&););3.9COM3.9.1COM接口□一定请使用IFACEMTHODIMP和IFACEMTHODIMP一作为COM接口的方法声明。这些宏替代了STDMETHODIMP和STDMETHOD,因为他们增加了_overrideSAL注释。示例:classCNetDataObj:publicIDataObject(public://IDataObjectIFACEMETHODIMPGetData(FORMATETC♦pFmtEtc,STGMEDIUM*pmedium)IFACEMETHODIMPCNetDataObj::GetData(FORMATETC*pFmtEtC,STGMEDIUM*pmedium){)0一定请将类定义中接口方法的顺序与声明中的顺序保持一致。以下是IUnknown的顺序:Querylnterface()/AddRef()/Release()o3.9.2COM接口ID_uuidof()是一个编译器支持的特性,其产生一个可能与类型相关的GUID值。该类型可能是接口指针,或者类。该GUID通过_declspec(uuidof-(" 400一定请在实现了COM对象的类的构造函数里,将m_cRef初始化为1。(注意:ATL使用了不同的引用计数的初始化模式,将其设为〇,并期望智能指针的赋值会将其增加到1)。这样便消除了类存在,却不能被释放的情况的发生。0一定请从每ー个COM方法中返回一个HRESULT〇(除了AddRef和Release)。3.9动态分配0一定请确保所有动态分配的内存会被相同的机制所释放。使用‘new,动态分配的对象,应该使用'delete’来释放。举例:Engine*pEngine=newEngine();pEngine->Process();deletepEngine;使用vectornew的动态分配应该使用vectordelete«举例:wchar_t*pszBuffer=newwchar_t[MAX_PATH];SomeMethod(pszBuffer);delete[]pszBuffer;0一定要理解您代码中的动态分配,以确保他们被正确的释放了。3.9.1智能指针0您应该使用RAII(初始化时分配资源)特性来帮助追踪动态分配一例如:使用智能指针。示例如下:CAutoPtr 41",hr);gotoCleanup;//FunctionreturnTRUE/FALSE,setWin32lasterroroDWORDdwError=ERROR_SUCCESS;HANDLEhToken=NULL; 42if(!OpenProcessToken(GetCurrentProcess(),TOKEN_QUERY|TOKEN_DUPLICATE,&hToken))(dwError=GetLastError();wprintf(L"OpenProcessTokenfailedw/err0x%081x 43",dwError);gotoCleanup;}3.9.1异常异常是C++的特性,在正确使用之前需要很好的理解它们。在使用带有本地C++异常的代码时,请确保您了解使用这些代码的含义。本地C++异常,是ー个C++语言强大的特性。可以降低代码复杂度,减少编写和维护的代码量。0一定请按值抛出异常,并按引用捕获异常。举例:voidProcessitem(constItem&item)(try(if(/*sometestfailed*/)(throw_com_error(E_FAIL);)}catch(_com_errorscomError)(//ProcesscomError//})。当重新抛出异常时,请使用"throw"来重抛,而不要使用"throwく捕获到的异常>"。举例,Good:voidProcessitem(constItem&item)(try(Item->Process();)catch(ItemException&itemException)(wcout< 44PatternExample-goto清理法HANDLEhToken=NULL;PVOIDpMem=NULL;if(!OpenProcessToken(GetCurrentProces(),TOKENQUERY,&hToken))(ReportError(GetLastError());gotoCleanup;}■pMem=LocalAlloc(LPTR,10);if(pMem==NULL)(ReportError(GetLastError());gotoCleanup;)■Cleanup:if(hToken)(CloseHandle(hToken);hToken=NULL;}if(pMem)(LocalFree(pMem);pMem=NULL;)_try/_finally法HANDLEhToken=NULL;PVOIDpMem=NULL;_try{if(!OpenProcessToken(GetCurrentProces(),TOKENQUERY,ShToken))(ReportError(GetLastError());leave;}一pMem=LocalAlloc(LPTR,10);if(pMem==NULL)(ReportError(GetLastError());leave;「finally(■if(hToken)(下,隐藏错误是可接受的,但是这些情况的出现几率非常低。只捕获函数能处理的特定异常。所有其他异常都不用处理。図一定不要在控制流中使用异常。除了系统故障或者带有潜在资源竞争条件的操作,您编写的代码都不应该抛岀异常。比如,在调用可能失败或抛出异常的方法前,您可以检查其前置条件,举例:if(IsWritable(list))(WriteList(list);}□一定要确保您理解可能从您所依赖的代码中抛出的异常,并确保异常不会无故传递至使用您的API的用户代码中。比如,STL和ATL在ー些特定情况下,可能抛出本地C++异常一了解这些情况,确保您的代码恰当地处理了这些异常,以防止其向外传递。3.9资源清理动态分配的内存或资源,在您退出函数之前应该被正确的清理,以防止内存或资源泄露。在函数执行过程中出现错误时,恰当的资源清理方案是非常重要的。以下是在函数中,5种常见的资源清理模式。 45CloseHandle(hToken);hToken=NULL;)if(pMem)(LocalFree(pMem);pMem=NULL;}}带RAII封装器的提前返冋法namespaceWinRAII{classAutoFreeObjHandle(public:explicitAutoFreeObjHandlem_hObj(h){;}-AutoFreeObjHandle(){ClcvoidClose(void)HANDLEh)se();}嵌套if法HANDLEhToken=NULL;PVOIDpMem=NULL;if(OpenProcessToken(GetCurrentPrcTOKENQUERY,&hToken))(pMem=LocalAlloc(LPTR,10);if(pMem)(LocalFree(pMem);pMem=NULL;}else(ReportError(GetLastError());}CloseHandle(hToken);hToken=NULL;}else(ReportError(GetLastError());''■(cess(),嵌套ifM经常是最坏的选择。您会很快耗尽水平空间•代码琏难攻13囉成)(CloseHandle(m_hObj);mjObj=NULL;})HANDLEGet(void)const{return(m__hObj);}voidSet(HANDLEh){m_h0bprivate:HANDLEm_h0bj;AutoFreeObjHandle(void);AutoFreeObjHandle(constAutoFreeObjHandle&AutoFreeObjHandle&operato(constAutoFreeObjHandle}}WinRAII::AutoFreeObjHandleafToif(!OpenThreadToken(GetCurreTOKENQUERY,TRUE,SchToken)){ReportError(GetLastError())rptiirnFAT.SR;j=h;});r=&);ken(NULL)ntThread(i带重复清理的提DWORDdwError=ERROR_SUCCESS;提附返回法在C中效率低下,因为其以重复清前返冋法HANDLEhToken=NULL;PVOIDpMem=NULL;,,3.18換制割if(!OpenProcessToken(GetCurrentProcess(),TOKEN_QUERY|TOKEN_DUPLICATErSchToken))ReportError(GetLastError());ユ•18」提冃リ1returnFALSE;図您不应该存pMem=LocalAlloc(LPTR,10);〈PMem==NULL)提前返回法发ReportError(GetLastError());CloseHandle(hToken);用。理想状;hToken=NULL;}returnFALSE;有一个返回1CloseHandle(hToken);回。LocalFree(pMem);returnTRUE;理代码来结束・如果函数没打淸理工作,或代码量非常少,那么可以使用该清理方法.区回・ー个方法内提前返回。某些情况下,と可接受的,但是应该避免对其滥氐下,所有函数都应该只在函数底部マ,所有的执行路径都会通过该点返 46可以使用提前返回的情况如下:•在函数开头的参数验证。»代码量极其少的函数(例如,成员变量状态访问器)3.18.2Goto図一定不要使用‘got。’语句来代替结构化控制流以企图优化运行时性能表现。这样做,会导致代码难于阅读,调试和验证。す您可以以向前跳转,并实现ー个跳转至同一目的地(例如,当某资源无法分配时,跳出资源分配代码,进入资源清理代码)的跳转集合来实现结构化的风格。这样使用goto便能减少嵌套深度,使得错误处理更易理解和验证。示例:BOOLIsElevatedAdministrator(HANDLEhlnputToken)(BOOLfIsAdmin=FALSE;HANDLEhTokenToCheck=NULL;//Ifcallersuppliesatoken,duplicateittoanimpersonationtoken//becauseCheckTokenMembershiprequiresanimpersonationtoken.if(hlnputToken)(if(!DuplicateToken(hlnputToken,SecurityIdentification,ShTokenToCheck))(gotoCLEANUP;))DWORDsidLen=SECURITY_MAX_SID_SIZE;BYTElocalAdminsGroupSid[SECURITY_MAX_SID_SIZE];if(!CreateWellKnownSid(WinBuiltinAdministratorsSid,NULL,localAdminsGroupSid,&sidLen))(gotoCLEANUP;)//Now,determmineiftheuserisanadminif(CheckTokenMembership(hTokenToCheck,localAdminsGroupSid,&fIsAdmin))(//lastErr=ERROR_SUCCESS;)CLEANUP://Closetheimpersonationtokenonlyifweopenedit.if(hTokenToCheck)(CloseHandle(hTokenToCheck);hTokenToCheck=NULL;)return(fIsAdmin);)逻辑上,代码被结构化的分为4块(如下阴影)。前2块代码试图分配资源,如果成功,控制流则执行下ー块代码。如果某一块代码中资源分配失败,控制流则执行清理代码。第3块代码决定函数最终结果,之后执行清理代码。如上,我们便结构化的使用了goto,因为所有goto都是向前跳转,并一致的出于同一个目的。结果代码 47相较于嵌套方法非常简短,易于阅读和验证。这便是goto的正确用法。BOOLIsElevatedAdministrator(HANDLEhlnputToken){BOOLflsAdmin=FALSE;HANDLEhTokenToCheck=NULL;//Ifcallersuppliesatoken,duplicateittoanimpersonationtoken//becauseCheckTokenMembershiprequiresanimpersonationtoken.if(hlnputToken)(if(!DuplicateToken(hlnputToken,Securityldentification,ShTokenToCheck))(gotoCLEANUP;))DWORDsidLen=SECURITY_MAX__SID_SIZE;BYTElocalAdminsGroupSid[SECURITY_MAX_SID_SIZE];CLEANUP://Closetheimpersonationtokenonlyifweopenedit.if(hTokenToCheck)(CloseHandle(hTokenToCheck);hTokenToCheck=NULL;}return(flsAdmin);if(!CreateWellKnownSid(WinBuiltinAdministratorsSid,NULL,localAdminsGroupSid,&sidLen))(gotoCLEANUP;)//Now,determmineiftheuserisanadminif(CheckTokenMembership(hTokenToCheck,localAdminsGroupSid,&fIsAdmin))(//lastErr=ERROR_SUCCESS;) 484.NET编码规范以下编程规范适用于C#和VB.NET。将copyright设置为Copyright©MicrosoftCorporation2010[assembly:AssMicrosoftCorf将AssemHyCompany设置为Microsoft[assembly:AssCorporationCorporation")将AssemblyTitle和AssemblyProduct设置为当[assembly:Ass前示例名[assembly:Ass4.1类库开发设计规范MSDN上的类库开发设计规范对如何编写优秀的托管代码进行了细致的讨论。本章节的内容着重于一些重要的规范,以及ー站式代码示例库对于该规范的ー些例外情况。因此,建议你同时参照这两份文档。4.2文件和结构S一定不要在一个源文件内拥有一个以上的Public类型,除非它们只有泛型参数个数的差别,或者具有嵌套关系。一个文件内有多个内部类型是允许的。0一定请以源文件所含的Public类名命名该文件。比如,MainForm类应该在MainForm.cs文件内,而List 49}13一定请仅为命名资源使用字母数字字命名空间Pascal规范幺词_________动线〇namespace図一定不要以相同的名称来命名命名空间和其内部的类型。—十名词4.4.3匈牙加団一定请以复数名词或名词短语来命名标志枚举,以单数名词或名词短语来絵逆窜史眄ヨ1X1ス」イ、^^^vMicrosoft.Sample.Windows714タ注枚举Pascal规范publicenumConsoleModifiers小前年他用好切]命名法(例如,方法Pascal规范动词或动词短语不要在变量名publicvoidPrint(){•••}州測作其契輯緜将ケ賛〇Public属性Pascal规范名词或形容词回一定请以集合中项目的复数形式句名该集团ス合,或者单数名词后面跟“Lisビ食客4尸界山“Collection”。0一定请以肯定短语来命名布尔属性,、(CanSeek,而不是CantSeek)用‘髭控件应该“Can,"or"Has"作布尔属性的前缀有意义时,您也可以这样做0码更易读publicstringCustomerName一朝強醉3SectionItems副生布名無QCanRead使用如下前缀,其重要目的是使代非Public属性Came!规范或_camel规范名词或形容词団一定请在您使用〕前缀时,保持代码・致privatestringname;privatestring_name;住。|控件类型前缀事件Pascal规范动词或动词短语0一定请用现在式或过去式来表耳円!的史前或//Araiscloseeventthatterthewindov,sris是之后的概念。ChprkRnx図一定不要使用“Before"或者“品;”前嬲后缀来指明事件的先后。CheckedListBComboBoxclospublox//Arais空hkiceventWindowClos1stc1oseevent-tha&——;で而bforeawindowsedsisContextMeniclospublfd.i(m@UentWindowClos:ingDataGrirld2委托Pascal规范回一定请为用r事件的委托增,EventHandler后缀。,rZJllDateTimePickpublewindicdelegate□uitposedEventHandlesr团一定请为除了用于事件处理アヰ脐井的委托suffix:XXXForm■*・目〃ロしcniuaしノロt双。図一定不要为委托增加〃Dele能电P居缪_grp接口Pasca!规范,带%诃ImageListpubliirplnterfaceIDictionary有’1‘前缀Labellb常量Pascal规范用于Public常量;Came!规范用于Internal常量;只有1或2个字符名诃ListBoxpubli牛Sビ。nststringageText-11A",ListViewpriv川WA^onststringMenuITIESSpubli 50StatusBarstsTabControltabTabPagetabTextBoxtbTimertmrTreeViewtvwpublicclassShellFolder(publicstaticreadonlyShellFolderProgramData=newShellFolder("ProgramData11);publicstaticreadonlyShellFolderProgramFiles=newShellFolder(”ProgramData");比如,对于"FileISave"菜单选线,"Save"菜单项应该命名为"mnuFileSave”。4.1常量0一定请将那些永远不会改变值定义为常量字段。编译器直接将常量字段嵌入调用代码处。所以常量值永远不会被改变,且并不会打破兼容性。publicclassInt32(publicconstintMaxValue=0x7fffffff;publicconstintMinValue=unchecked((int)0x80000000);)PublicClassInt32PublicConstMaxValueAsInteger=&H7FFFFFFFPublicConstMinValueAsInteger=&H80000000EndClass回一定请为预定义的对象实例使用publicstatic(shared)readonly字段。如果有预定义的类型实例,也将该类型定义为publicstaticreadonly。举例,PublicClassShellFolderPublicSharedReadonlyProgramDataAsNewShellFolder("ProgramData")PublicSharedReadonlyProgramFilesAsNewShellFolder("ProgramFiles")EndClass4.2字符串図一定不要使用‘+‘操作符(VB.NET中的,&,)来拼接大量字符串。相反,您应该使用StringBuilder来实现拼接工作。然而,拼接少量的字符串时,一定请使用屮操作符(VB.NET中的&)〇Good:StringBuildersbXML=newStringBuilder();sbXML.Append(" 51Stringcomparison.Ordinal和StringComparison.OrdinallgnoreCase作为您的安全默认值,以提高性能表现。回一定请在向用户输出结果时,使用基于Stringcomparison.Currentculture的字符串操作。回一定请在比较语言无关字符串(符号,等)时,使用非语言学的Stringcomparison.Ordina!或者StringComparison.OrdinallgnoreCase值,而不是基于Cultureinfo.Invariantculture的字符串操作。一般而言,不要使用基于Stringcomparison.Invariantculture的字符串操作。一个例外是当你坚持其语言上有意义,而与具体文化无关的情况。0一定请使用String.Equals的重载版本来测试2个字符串是否相等。比如,忽略大小写后,判断2个字符串是否相等,if(strl.Equals(str2,Stringcomparison.OrdinallgnoreCase))If(strl.Equals(str2,Stringcomparison.OrdinallgnoreCase))Then図一定不要使用String.Compare或CompareTo的重载版本来检验返回值是否为〇,来判断字符串是否相等。这2个函数是用于字符串排序,而非检查相等性。回一定请在字符串比较时,以String.ToUpperlnvariant函数使字符串规范化,而不用String.ToLowerlnvariant〇4.1数组和集合团您应该在低层次函数中使用数组,来减少内存消耗,增强性能表现。对于公开接口,则偏向选择集合。集合提供了对于其内容更多的控制权,可以随着时间改善,提高可用性。另外,不推荐在只读场景下使用数组,因为数组克隆的代价太高。然而,如果您把熟练开发者作为目标,对于只读场景使用数组也是个不错的主意。数组的内存占用比较低,这减少了工作区,并因为运行时的优化能更快的访问数组元素。図一定不要使用只读的数组字段。字段本身只读,不能被修改,但是其内部元素可以被修改。以下示例展示了使用只读数组字段的陷阱:Bad:publicstaticreadonlychar[]InvalidPathChars={•\,1<',•>•,'I');这允许调用者修改数组内的值:InvalidPathChars[0]='A*;您可以使用一个只读集合(只要其元素也是不可变的),或者在返回之前进行数组克隆。然而,数组克隆的代价可能过高:publicstaticReadOnlyCollection 52}publicstaticchar[]GetInvalidPathChars()(return(char[])badChars.Clone();}0您应该使用不规则数组来代替使用多维数组。ー个不规则数组是指其元素本身也是ー个数组。构成元素的数组可能有不同大小,这样相较于多维数组能减少ー些数据集的空间浪费(例如,稀疏矩阵)。另外,CLR能够对不规则数组的索引操作进行优化,所以在某些情景下,具有更好的性能表现。/Z不规则数组int[][]jaggedArray=(newint[]{1,2,3,4},newint[]{5,6,7},newint[]{8},newint[]{9});DimjaggedArrayAsInteger()()=NewInteger()(){一NewInteger(){1,2,3,4},_NewInteger(){5,6,7},_NewInteger(){8}r_NewInteger(){9}_)/Z多维数组int[,]multiDimArray=({1,2,3,4),(5,6,7,0},{8,0,0,0),{9,0,0,0));DimmultiDimArray(,)AsInteger={,{1,2,3,4},_{5,6,7,0},_{8,0,0,0),_{9,0,0,0}_0一定请将代表了读/写集合的属性或返回值声明为Collection 53个空数组或集合,而不是null引用。int[]arr=SomeOtherFunc();foreach(intvinarr)()4.1结构体0一定请确保将所有实例数据设置为〇值,false或者是null。当创建结构体数组时,这样能防止意外创建了无效实例。図ー定请为值类型实现IEquatable 54図您不应该从属性访问器内抛出异常。属性访问器应该只包含简单的操作,且不带任何前置条件。如果ー个属性访问器可能抛出异常,那么考虑将其重新设计为ー个方法。该推荐方法并不适用于索引器。索引器可以因为无效实参而抛出异常。从属性设置器内抛出异常是有效且可接受的。4.1.1构造函数0一定请尽量减少构造函数的工作量。除了得到构造函数参数,设置主要数据成员,构造函数不应该有太多的工作量。其余工作量应该被推迟,直到必须。团一定请在恰当的时候,从实例构造函数内抛出异常。团一定请在需要默认构造函数的情况下,显式的声明它。即使有时编译器为自动的为您的类增加一个默认构造函数,但是显式的声明使得代码更易维护。这样即使您增加了一个带有参数的构造函数,也能确保默认构造函数仍然会被定义。図一定不要在对象构造函数内部调用虚方法。调用虚方法时,实际调用了继承体系最底层的覆盖(override)方法,而不考虑定义了该方法的类的构造函数是否。被调用。4.1.2方法□一定请将所有输出参数放置于所有传值和传引用参数(除去参数数组)的之后,即使它引起了重载方法之前不一致的参数顺序。0一定请验证传递给Public,Protected或显式实现的成员方法的实参。如果验证失败,则抛出System.ArgumentException,或者其子集:如果ー个null实参传递给成员,而成员方法不支持null实参,则抛出ArgumentNullException。如果实参值超出由调用方法定义的可接受范围,则抛出ArgumentOutOfRangeException异常。4.1.3事件0一定请注意事件处理方法中可能会执行任意代码。考虑将引发事件的代码放入一个try-catch块中,以避免由事件处理方法中抛出的未处理异常引起的程序终止。図一定不要在有性能要求的AP!中使用事件。虽然事件易于理解和使用,但是就性能和内存消耗而言,它们不如虚函数。4.1.4成员方法重载0一定请使用成员方法重载,而不是定义带有默认参数的成员方法。默认参数并不是CLS兼容的,所以不能被某些语言重用。同时,带有默认参数的成员方法存在一个版本问题。我们考虑成员方法的版本1将可选参数默认设置为!23〇当编译代码调用该方法,且没有指定可选参数时,编译器 55在调用处直接将123嵌入代码中。现在,版本2将默认参数修改为863,如调用代码没有重新编译,那么它会调用版本2的方法,并传递123作为其参数。(123是版本1的默认参数,而不是版本2的。)。Good:PublicOverloadsSubRotate(ByVaidataAsMatrix)Rotate(data,180)EndSubPublicOverloadsSubRotate(ByVaidataAsMatrix,ByVaidegreesAsInteger)1DorotationhereEndSubBad:PublicSubRotate(ByVaidataAsMatrix,OptionalByVaidegreesAsInteger=180),DorotationhereEndSub図一定不要任意改动重载方法中的参数名。如果ー个重载函数中的参数代表着另一个重载函数中相同的参数,该参数则应该有相同的命名。具有相同命名的参数应该在重载函数中出现在同一位置。团一定请仅将最长重载函数设为虚函数(为了拓展性考虑)。短重载函数应该一直调用到长重载函数。4.1.1接口成员図您不应该在没有合理理由的情况下显式的实现接口成员。显式实现的成员可能使开发者感到困惑,因为他们不会出现在Public成员列表内,且会造成对值类型不必要的装箱拆箱操作。团您应该在成员只通过接口来调用的情况下,显式的实现成员接口。4.1.2虚成员方法相较于回调和事件,虚成员方法性能上有更好的表现。但是比非虚方法在性能上低一点。図一定不要在没有合理理由的情况下,将成员方法设置为虚方法,您必须意识到相关设计,测试,维护虚方法带来的成本。0您应该倾向于为虚成员方法设置为Protected的访问性,而不是Public访问性。Public成员应该通过调用Protected的虚方法来提供拓展性(如果需要的话)。4.1.3静态类団一定请合理使用静态类。静态类应该被用于框架内基于对象的核心支持辅助类。4.1.4抽象类図一定不要在抽象类中定义Public或Protected-Internal的构造函数。0一定请为抽象类定义ー个Protected,或Internal构造函数。 56Protected构造函数更常见,因为其允许当子类创建时,基类可以完成自己的初始化工作。publicabstractclassClaim(protectedClaim()(}}internal构造函数用于限制将抽象类的实现具化到定义该类的程序集。publicabstractclassClaim(internalClaim()(})4.1命名空间0一定请在ー站式代码示例库屮使用VisualStudio创建的项目的默认的命名空间。无需重命名命名空间为Microsoft.Sample.TechnologyNameo4.2错误和异常4.2.1抛出异常団一定请通过抛出异常来告知执行失败。异常在框架内是告知错误的主要手段。如果一个成员方法不能成功的如预期般执行,便应该认为是执行失败,并抛出ー个异常。一定不要返回一个错误代码。团一定请抛出最明确,最有意义的异常(继承体系最底层的)。比如,如果传递了一null实参,则抛岀ArgumentNullException,而不是其基类ArgumentException〇抛出并捕获System.Exception异常通常都是没有意义的。図一定不要将异常用于常规的控制流。除了系统故障或者带有潜在竞争条件的操作,您编写的代码都不应该抛出异常。比如,在调用可能失败或抛出异常的方法前,您可以检查其前置条件,举例,//c#示例:if(collection!=null&&!collection.IsReadOnly)(collection.Add(additionalNumber);}'VB.NET示例:If((NotcollectionIsNothing)And(Notcollection.IsReadOnly))Thencollection.Add(additionalNumber)EndIf図一定不要从异常过滤器块内抛出异常。当ー个异常过滤器引发了一个异常时,该异常会被CLR捕获,该过滤器返回false。该行为很难与过滤器显式的执行并返回错误区分开,所以会增加调试难度。•VB.NETsample,Thisisbaddesign.Theexceptionfilter(Whenclause),maythrowanexceptionwhentheInnerExceptionproperty'returnsnullTryCatcheAsArgumentException_When 57e.InnerException.Message.StartsWith(“File”)EndTry図一定不要显式的从finally块内抛出异常。从调用方法内隐式的抛出异常是可以接受的。4.11.2异常处理図您不应该通过捕获笼统的异常,例如System.Exception,System.SystemException,或者.NET代码中其他异常,来隐藏错误。一定要捕获代码能够处理的、明确的异常。您应该捕获更加明确的异常,或者在Catch块的最后一条语句处重新抛出该普通异常。以下情况隐藏错误是可以接受的,但是其发生几率很低Good://C#sample:trycatch(System.NullReferenceExceptionexc)(}catch(System.ArgumentoutOfRangeExceptionexc){)catch(System.InvalidCastExceptionexc)()•VB.NETsample:TryCatchexcAsSystem.NullReferenceExceptionCatchexcAsSystem.ArgumentOutOfRangeExceptionCatchexcAsSystem.InvalidCastExceptionEndTryBad://C#sample:try(}catch(Exceptionex)()'VB.NETsample:TryCatchexAsExceptionEndTry团一定请在捕获并重新抛出异常时,倾向使用throw»这是保持异常调用栈的最佳途径:Good://C#sample:try(...//Dosomereadingwiththefile)catch(file.Position=position;//Unwindonfailurethrow;//Rethrow)'VB.NETsample:Try...1DosomereadingwiththefileCatchexAsExceptionfile.Position=position'UnwindonfailureThrow•RethrowEndTryBad://C#sample:try(...//Dosomereadingwiththefile}catch(Exceptionex)(file.Position=position;//Unwindonfailurethrowex;//Rethrow 58'VB.NETsample:Try...1DosomereadingwiththefileCatchexAsExceptionfile.Position=position'UnwindonfailureThrowex'RethrowEndTry4.12资源清理図一定不要使用GC.Collect来进行强制垃圾回收。4.12.1Try-finally块0一定请使用try-finally块来清理资源,try-catch块来处理错误恢复。一定不要使用catch块来清理资源。一般来说,清理的逻辑是回滚资源(特别是原生资源)分配。举例://C#sample:FileStreamstream=null;try(stream=newFileStream(...);)finally(if(stream!=null)stream.Close();)'VB.NETsample:DimstreamAsFileStream=NothingTrystream=NewFileStream(...)CatchexAsExceptionIf(streamIsNotNothing)Thenstream.Close()EndIfEndTry为了清理实现了[Disposable接口的对象,C#和VB.NET提供了using语句来替代try-finally块。//C#sample:using(Filestreamstream=newFilestream(...))()•VB.NETsample:UsingstreamAsNewFilestream(...)EndUsing许多语言特性都会自动的为您写入try-finally块。例如C#/VB的using语句,C#的lock语句,VB的SyncLock语句,C#的foreach语句以及VBForEach语句。4.12.2基础Dispose模式该模式的基础实现包括实现System.(Disposable接ロ,声明实现了所有资源清理逻辑的Dispose(bool)方法,该方法被Dispose方法和可选的终结器所共享。请注意,本章节并不讨论如何编写・个终结器。可终结类型是该简单模式的拓展,我们会在下个章节中讨论。如下展示了基础模式的简单实现://C#sample:publicclassDisposableResourceHolderIDisposable(privatebooldisposed=false;privateSafeHandleresource;//HandletoaresourcepublicDisposableResourceHolder()(this.resource=...//Allocatesthenativeresource} 59publicvoidDoSomething(){if(disposed)thrownewObjectDisposedException(...);//NowcallsomenativemethodsusingtheresourcepublicvoidDispose(){'Dispose(true);GC.SuppressFinalize(this);}protectedvirtualvoidDispose(booldisposing){//Protectfrombeingcalledmultipletimes.if(disposed)return;if(disposing) 60//Cleanupallmanagedresources.if(resource!=null)resource.Dispose();)disposed=true;}}'VB.NETsample:PublicClassDisposableResourceHolderImplementsIDisposablePrivatedisposedAsBoolean=FalsePrivateresourceAsSafeHandle•HandletoaresourcePublicSubNew()resource=...•AllocatesthenativeresourceEndSubPublicSubDoSomething()If(disposed)ThenThrowNewObjectDisposedException(...)EndIf'NowcallsomenativemethodsusingtheresourceEndSubPublicSubDispose()ImplementsIDisposable.DisposeDispose(True)GC.SuppressFinalize(Me)EndSubProtectedOverridableSubDispose(ByVaidisposingAsBoolean)'Protectfrombeingcalledmultipletimes.IfdisposedThenReturnIfdisposingThen'Cleanupallmanagedresources.If(resourceIsNotNothing)Thenresource.Dispose()EndIfEndIfdisposed=TrueEndSubEndClass0一定请为包含了可释放类型的类型实现基础Dispose模式。团一定请拓展基础Dispose模式来提供ー个终结器。比如,为存储非托管内存的缓冲实现该模式。0您应该为即使类本身不包含非托管代码或可清理对象,但是其子类可能带有的类实现该基础Dispose模式。ーー个绝佳的例子便是System1〇.Stream类。虽然它是ー个不带任何资源的抽象类,大多数子类却带有资源,所以应该为其实现该模式。0一定请声明ー"个protectedvirtualvoidDispose(booldisposing)方法来集中所有释放非托管资源的逻辑。所有资源清理都应该在该方法中完成。用终结器和IDisposable.Dispose方法来调用该方 61法。如果从终结器内调用,则其参数为false。它应该用于确保任何在终结中运行的代码不应该被其他可终结对象访问到。下一章节我们会详细介绍终结器的细节。//C#sample:protectedvirtualvoidDispose(booldisposing){if(disposing)(//Cleanupallmanagedresources.if(resource!=null)resource.Dispose();}}}'VB.NETsample:ProtectedOverridableSubDispose(ByVaidisposingAsBoolean),Protectfrombeingcalledmultipletimes.IfdisposedThenReturnIfdisposingThen1Cleanupallmanagedresources.If(resourceIsNotNothing)Thenresource.Dispose()EndIfEndIfdisposed=TrueEndSubEl一定请通过简单的调用Dispose(true)»以及GC.SuppressFinalize(this)来实现[Disposable接□〇仅当Dispose(true)成功执行完后才能调用SuppressFinalize〇//C#sample:publicvoidDispose()(Dispose(true);GC.SuppressFinalize(this);)'VB.NETsample:PublicSubDispose()ImplementsIDisposable.DisposeDispose(True)GC.SuppressFinalize(Me)EndSub図一定不要将无参Dispose方法定义为虚函数。Dispose(bool)方法应该被子类重写。図您不应该从Dispose(bool)内抛出异常,除非包含它的进程被破坏(内存泄露,不一致的共享状态,等等)等极端条件。用户不希望调用Dispose会引发异常。比如,考虑在C#代码内手动写入try-finally块:TextReadertr=newStreamReader(File.OpenRead("foo.txt”));try(//Dosomestuff)finally(tr.Dispose();//Morestuff)如果Dispose可能引发异常,那么finally块的清理逻辑不会被执行。为了解决这一点,用户需要将所有对于Dispose(在它们的finally块内!)的调用放入try块内,这将导致一个非常复杂的清理处理程序。如果执行Dispose(booldisposing)方法,即使终结失败也不会抛出异常。如果在ー个终结器环境内这样做会终止当前流程。团一定请在对象被终结之后,为任何不能再被使用 62的成员抛出Iー个ObjectDisposedException异常。//C#sample:publicclassDisposableResourceHolder:IDisposable(privatebooldisposed=false;privateSafeHandleresource;//HandletoaresourcepublicvoidDoSomething()(if(disposed)thrownewObjectDisposedException(...);//Nowcallsomenativemethodsusingtheresource)protectedvirtualvoidDispose(booldisposing)(if(disposed)return;//Cleanupdisposed=true;}'VB.NETsample:PublicClassDisposableResourceHolderImplementsIDisposablePrivatedisposedAsBoolean=FalsePrivateresourceAsSafeHandle'HandletoaresourcePublicSubDoSomething()If(disposed)ThenThrowNewObjectDisposedException(...)EndIf,NowcallsomenativemethodsusingtheresourceEndSubProtectedOverridableSubDispose(ByVaidisposingAsBoolean)*Protectfrombeingcalledmultipletimes.IfdisposedThenReturn*Cleanupdisposed=TrueEndSubEndClass4.12.3可终结类型可终结类型是通过重写终结器并在Dispose(bool)中提供终结代码路径来拓展基础Dispose模式的类型。如下代码是ー个可终结类型的示例: 63//C#sample:publicclassComplexResourceHolder:IDisposable(booldisposed=false;privateIntPtrbuffer;//UnmanagedmemorybufferprivateSafeHandleresource;//DisposablehandletoaresourcepublicComplexResourceHolder()(this.buffer=...//Allocatesmemorythis.resource=...//Allocatestheresource}publicvoidDoSomething()(if(disposed)thrownewObjectDisposedException(...);//Nowcallsomenativemethodsusingtheresource-ComplexResourceHolder(){-Dispose(false);}publicvoidDispose(){Dispose(true);GC.SuppressFinalize(this);}protectedvirtualvoidDispose(booldisposing){'//Protectfrombeingcalledmultipletimes.if(disposed)return;if(disposing)(//Cleanupallmanagedresources.if(resource!=null)resource.Dispose();)//Cleanupallnativeresources.ReleaseBuffer(buffer);disposed=true;•VB.NETsample:PublicClassDisposableResourceHolderImplementsIDisposablePrivatedisposedAsBoolean=FalsePrivatebufferAsIntPtr,UnmanagedmemorybufferPrivateresourceAsSafeHandle'HandletoaresourcePublicSubNew()buffer=...'Allocatesmemoryresource=...'AllocatesthenativeresourceEndSubPublicSubDoSomething()If(disposed)ThenThrowNewObjectDisposedException(...)EndIf,NowcallsomenativemethodsusingtheresourceEndSubProtectedOverridesSubFinalize 64Dispose(False)MyBase.Finalize()EndSubPublicSubDispose()ImplementsIDisposable.DisposeDispose(True)GC.SuppressFinalize(Me)EndSubProtectedOverridableSubDispose(ByVaidisposingAsBoolean)'Protectfrombeingcalledmultipletimes.IfdisposedThenReturnIfdisposingThen1Cleanupallmanagedresources.If(resourceIsNotNothing)Thenresource.Dispose()EndIfEndIf'Cleanupallnativeresources.ReleaseBuffer(Buffer)disposed=TrueEndSubEndClass0一定请在类型应该为释放非托管资源负责,且自身没有终结器的情况下,将该类型定义为可终结的。当实现终结器时,简单的调用Dispose(false),并将所有资源清理逻辑放入Dispose(booldisposing)方法。//C#sample:publicclassComplexResourceHolder:IDisposable(-ComplexResourceHolder()(Dispose(false);)protectedvirtualvoidDispose(booldisposing)•VB.NETsample:PublicClassDisposableResourceHolderImplementsIDisposableProtectedOverridesSubFinalize()Dispose(False)MyBase.Finalize()EndSubProtectedOverridableSubDispose(ByVaidisposingAsBoolean)EndSubEndClass0一定请谨慎的定义可终结类型。仔细考虑任何ー个您需要终结器的情况。带有终结器的实例从性能和复杂性角度来说,都需付出不小的代价。0一定请为每ー个可终结类型实现基础Dispose模式。该模式的细节请参考先前章节。这给予该 65类型的使用者以ー种显式的方式去清理其拥有的资源。図您应该在终结器即使面临强制的应用程序域卸载或线程中止的情况也必须被执行时,创建并使用临界可终结对象(ー个带有包含了CriticalFinalizerObject的类型层次的类型)。0一定请尽量使用基于SafeHandle或SafeHandleZeroOrMinusOnelsInvalid(对于Win32资源句柄,其值如果为0或者ー:L,则代表其为无效句柄)的资源封装器,而不是自己来编写终结器。这样,我们便无需终结器,封装器会为其资源清理负责。安全句柄实现了ルisposable接口,并继承自CriticalFinalizerObject»所以即使面临强制的应用程序域卸载或线程中止,终结器的逻辑也会被执行。III 66}publicSafeLocalMemHandle(IntPtrpreexistingHandle,boolownsHandle):base(ownsHandle)(base.SetHandle(preexistingHandle);}[Reliabilitycontract(Consistency.WillNotCorruptState,Cer.Success),DllImport(“kernel32.dll”,CharSet=CharSet.Auto,SetLastError=true)]privatestaticexternIntPtrLocalFree(IntPtrhMem);protectedoverrideboolReleaseHandle()(return(LocalFree(base.handle)==IntPtr.Zero);}s一定不要在终结器代码路径内访问任何可终结对象,因为这样会有一个极大的风险:它们已经被终结了。例如,ー个可终结对象A,其拥有一个指向另一个可终结对象B的对象,在A的终结器内并不能很安心的使用B,反之亦然。终结器是被随机调用的。但是操作拆箱的值类型字段是可以接受的。同时也请注意,在应用程序域卸载或进程退出的某些时间点上,存储于静态变量的对象会被回收。如果Environment.HasShutdownStarted返回True,则访问引用了一个可终结对象的静态变量(或调用使用了存储于静态变量的值的静态函数)可能会不安全。図一定不要从终结器的逻辑内抛出异常,除非是系统严重故障。如果从终结器内抛出异常,CLR可能会停止整个进程来阻止其他终结器被执行,并阻止资源以ー个受控制的方式释放。4.12.4重写Dispose如果您继承了实现[Disposable接口的基类,您必须也实现【Disposable接口。记得要调用您基类的Dispose(bool)〇publicclassDisposableBase:IDisposable(-DisposableBase()(Dispose(false);}publicvoidDispose()(Dispose(true);GC.SuppressFinalize(this);}protectedvirtualvoidDispose(booldisposing){//...})publicclassDisposableSubclass:DisposableBaseprotectedoverridevoidDispose(booldisposing)(try(if(disposing){//Cleanupmanagedresources. 67)//Cleanupnativeresources.}finally(base.Dispose(disposing);}})4.13交互操作4.13.1P/lnvoke0一定请在编写P/lnvoke签名时,查阅P/lnvoke交互操作助理以及http:〃pinvoke.net。0您可以使用IntPtr来进行手动封送处理。虽然会牺牲易用性,类型安全和维护性,但是通过将参数和字段声明为IntPtr,您可以提升性能表现。有时,通过Marshal类的方法来执行手动封送处理比依赖默认交互操作封送处理的性能更高。举例来说,如果有一个字符串的大数组需要被传递跨越交互操作边界,但是托管代码只需其中几个元素,那么您就可以将数组声明为IntPtr,手动的访问所需的几个元素。図一定不要锁定短暂存在的对象。锁定短暂存在的对象会延长在P/lnvoke调用时内存缓冲区的生存期。锁定会阻止垃圾回收器重新分配托管堆内对象内存,或者是托管委托的地址。然而,锁定长期存在的对象是可以接受的,因为其是在应用程序初始化期间创建的,相较于短暂存在对象,它们并不会被移动。长期锁定短暂存在的对象代价非常高昂,因为内存压缩经常发生在第。代的垃圾回收时,垃圾冋收器不能重新分配被锁定的对象。这样会造成低效的内存管理,极大的降低性能表现。更多复制和锁定请参考http://msdn.microsoft.com/en-us/library/23acw07k.aspxo0一定请在P/lnvoke签名中将CharSet设为CharSet.Auto,SetLastError设为true。举例,//C#sample:[Dlllmport("kernel32.dll",CharSet=CharSet.Auto,SetLastError=true)]publicstaticexternSafeFileMappingHandleOpenFileMapping(FileMapAccessdwDesiredAccess,boolblnheritHandle,stringIpName);•VB.NETsample: 68[Securitypermission(SecurityAction.LinkDemand,UnmanagedCode=true)]privateSafeFileMappingHandle():base(true)(}[Securitypermission(SecurityAction.LinkDemand,UnmanagedCode=true)]publicSafeFileMappingHandle(IntPtrhandle,boolownsHandle):base(ownsHandle)(base.SetHandle(handle);}[Reliabilitycontract(Consistency.WilINotCorruptstate,Cer.Success),Dlllmport("kernel32.dll”,CharSet=CharSet.Auto,SetLastError=true)][return:MarshalAs(UnmanagedType.Bool)]privatestaticexternboolCloseHandle(IntPtrhandle);protectedoverrideboolReleaseHandle()(returnCloseHandle(base.handle);})'', 69finally{if(hMapFile!=null)(//Closethefilemappingobject.hMapFile.Close();hMapFile=null;)}'VB.NETsample:DimhMapFileAsSafeFileMappingHandle=NothingTry•Trytoopenthenamedfilemapping.hMapFile=NativeMethod.OpenFileMapping(_FileMapAccess.FILE_MAP_READ,False,_FULL_MAP_NAME)If(hMapFile.Islnvalid)ThenThrowNewWin32ExceptionEndIfFinallyIf(NothMapFileIsNothing)Then1Closethefilemappingobject.hMapFile.Close()hMapFile=NothingEndIfEndTry 704.13.2COM交互操作0一定不要以GCCollect来进行强制垃圾冋收以释放有性能要求的API内的COM对象。释放COM对象一个常见的方法是将RCW引用设置为null,并调用System.GC.Collect以及System.GC.WaitForPendingFinalizerSo如果对性能有较高要求,我们并不推荐这样做,因为这样会引起垃圾回收运行太频繁。如果在服务器应用程序代码中使用该方法则极大的降低了性能和可拓展性。您应该让垃圾回收器自己决定何时执行垃圾回收。0您应该使用Marshal.FinalReleaseComObject或者Marshal.ReleaseComObject来手动管理RCW的生存周期。相较与GC.Collect来进行强制垃圾冋收,这样做具有较高的效率。図一定不要进行跨套间调用。当您从托管应用程序调用ー个COM对象时,请确保托管代码的套间类型和COM对象套间类型是相匹配的。通过使用匹配的套间,您可以避免跨套间调用时的线程切换。
此文档下载收益归作者所有