1.开发流程
程序的Bug与瑕疵往往出现于开发流程当中。只要对工具善加利用,就有助于在你发布程序之前便将问题发现,或避开这些问题。
标准化代码书写
标准化代码书写可以使代码更加易于维护,尤其是在代码由多个开发者或团队进行开发与维护时,这一优点更加突出。常见的强制代码规范化的工具有:FxCop、StyleCop和ReSharper。
开发者语:在掩盖错误之前请仔细地思考这些错误,并且去分析结果。不要指望依靠这些工具来在代码中寻找错误,因为结果可能和你的与其相去甚远。
代码审查
审查代码与搭档编程都是很常见的练习,比如开发者刻意去审查他人书写的代码。而其他人很希望发现代码开发者的一些bug,例如编码错误或者执行错误。
审查代码是一种很有价值的练习,由于很依赖于人工操作,因此很难被量化,准确度也不够令人满意。
静态分析
静态分析不需要你去运行代码,你不必编写测试案例就可以找出一些代码不规范的地方,或者是一些瑕疵的存在。这是一种非常有效地寻找问题的方式,但是你需要有一个不会有太多误报问题的工具。C#常用的静态分析工具有Coverity,CAT,NET,Visual Studio CodeAnalysis。
动态分析
在你运行代码的时候,动态分析工具可以帮你找出这些错误:安全漏洞,性能与并发性问题。这种方法是在执行时期的环境下进行分析,正因如此,其有效性便受制于代码复杂度。Visual Studio提供了包括Concurrency Visualizer, IntelliTrace, and Profiling Tools在内的大量动态分析工具。
管理者/团队领导语:开发实践是练习规避常见陷阱的最好方法。同时也要注意测试工具是否符合你的需求。尽量让你团队的代码诊断水平处于可控的范围内。
测试
测试的方式多种多样:单元测试,系统集成测试,性能测试,渗透测试等等。在开发阶段,绝大多数的测试案例是由开发者或测试人员来完成编写,使程序可以满足需求。
测试只在运行正确的代码时才会有效。在进行功能测试的时候,它还可以用来挑战开发者的研发与维护速度。
开发最佳实践
工具的选择上多花点时间,用正确的工具去解决你关心的问题,不要为开发者增添额外的工作。让分析工具与测试自动流畅地运行起来去寻找问题,但是要保证代码的思想仍然清晰地留在开发者的头脑当中。
尽可能快地定位诊断出来的问题所在位置(不论是通过静态分析还是测试得到的错误,比如编译警告,标准违例,问题检测等)。如果刚出来的问题由于“不关心”而去忽略它,导致该问题后来很难找到,那么就会给代码审阅工作者增加很大的工作量,并且还要祈祷他们不会因此烦躁。
请接受这些有用的建议,让自己代码的质量,安全性,可维护性得到提升,同时也提升开发者们的研发能力、协调能力,以及提升发布代码的可预测性。
2.类型的陷阱
C#的一个主要的优点就是其灵活的类型系统,而安全的类型可以帮助我们更早地找到错误。通过强制执行严格的类型规则,编译器能够帮助你维持良好的代码书写习惯。在这一方面,C#语言与.NET框架为我们提供了大量的类型,以适应绝大多数的需求。虽然许多开发者对一般的类型有着良好的理解,并且也知晓用户的需求,但是一些误解与误用仍然存在。
更多关于.NTE框架类库的信息请参阅MSDN library。
理解并使用标准接口
特定的接口涉及到常用的C#特征。例如,IDiposable允许使用常见的资源管理语言,例如关键词“using”。良好地理解接口可以帮助你书写通顺的C#代码,并且更易于维护。
避免使用ICloneable接口——开发者从来没搞清楚一个被复制的对象到底是深拷贝还是浅拷贝。由于仍没有一种对复制对象操作是否正确的标准评判,于是也就没办法有意义地去将接口作为一个contract去使用。
结构体
尽量避免向结构体中进行写入,将它们视为一种不变的对象以防止混乱。在像多线程这种场景下进行内存共享,会变得更安全。我们对结构体采用的方法是,在创建结构体时对其进行初始化操作,如果需要改变其数据,那么建议生成一个新的实体。
正确理解哪些标准类型/方法是不可变,并且可返回新的值(例如串,日期),用这些来替代那些易变对象(如List.Enumerator)。
字符串
字符串的值可能为空,所以可以在合适的时候使用一些比较方便的功能。值判断(s.Length==0)时可能会出现NullReferenceException错误,而String.IsNullOrEmpty(s)和String.IsNullOrWhitespace(s)可以很好地使用null。
标记枚举
枚举类型与常量可以使代码更加易于阅读,通过利用标识符替换幻数,可以表现出值的意义。
如果你需要生成大量的枚举类型,那么带有标记的枚举类型是一种更加简单的选择:
[Flag]publicenumTag{None=0x0,Tip=0x1,Example=0x2}
下面这种方法可以让你在一个snippet中使用多重标记:
snippet.Tag = Tag.Tip | Tag.Example
这种方法有利于数据的封装,因此你也不必担心在使用Tag property getter时有内部集合信息泄露。
Equality comparisons(相等性比较)
有如下两种类型的相等性:
1.引用相等性,即两种引用都指向同一个对象。
2.数值相等性,即两个不同的引用对象可以视为相等的。
除此之外,C#还提供了很多相等性的测试方法。最常见的方法如下:
==与!=操作
由对象的虚继承等值法
静态Object.Equal法
IEquatable接口等值法
静态Object.ReferenceEquals法
有时候很难弄清楚使用引用或值相等性的目的。想进一步弄明白这些,并且让你的工作做得更好,请参阅:
MSDNhttp://msdn.microsoft.com/en-us/library/dd183752.aspx
如果你想要覆盖某个东西的时候,不要忘了MSDN上为我们提供的诸如IEquatable, GetHashCode()之类的工具。
注意无类型容器在重载方面的影响,可以考虑使用“myArrayList[0] == myString”这一方法。数组元素是编译阶段类型的“对象”,因此引用相等性可以使用。虽然C#会向你提醒这些潜在的错误,但是在编译过程中,unexpected reference equality在某些情况下不会被提醒。
3.类的陷阱
封装你的数据
类在恰当管理数据方面起很大的作用。鉴于性能上的一些原因,类总是缓存部分结果,或者是在内部数据的一致性上做出一些假设。使数据权限公开的话会在一定程度上让你去缓存,或者是作出假设,而这些操作是通过对性能、安全性、并发性的潜在影响表现出来的。例如暴露像泛型集合、数组之类的易变成员项,可以让用户跳过你而直接进行结构体的修改。
属性
除了可以通过access modifiers控制对象之外,属性还可以让你很精确地掌控用户与你的对象之间进行了什么交互。特别要指出的是,属性还可以让你了解到读写的具体情况。
属性能在通过存储逻辑将数据覆写进getters与setters的时候帮助你建立一个稳定的API,或是提供一个数据的绑定资源。
永远不要让属性getter出现异常,并且也要避免修改对象状态。这是一种对方法的需求,而不是属性的getter。
更多有关属性的信息,请参阅MSDN:
http://msdn.microsoft.com/en-us/library/ms229006(v=vs.120).aspx
同时也要注意getter的一些副作用。开发者也习惯于将成员体的存取视为一种常见的操作,因此他们在代码审查的时候也常常忽视那些副作用。
对象初始化
你可以为一个新创建的对象根据它创建的表达形式赋予属性。例如为Foo与Bar属性创建一个新的具有给定值的C类对象:
newC
你也可以生成一个具有特定属性名称的匿名类型的实体:
varmyAwesomeObject =new;
初始化过程在构造函数体之前运行,因此需要保证在输入至构造函数之前,将这一域给初始化。由于构造函数还没有运行,所以目标域的初始化可能不管怎样都不涉及“this”。
过渡规范细化的输入参数
为了使一些特殊方法更加容易控制,最好在你使用的方法当中使用最少的特定类型。比如在一种方法中使用 List进行迭代:
publicvoidFoo(List bars){foreach(varbinbars) {// do something with the bar...}}
对于其他IEnumerable集来说,使用这种方法的表现更加出色一些,但是对于特定的参数List来说,我们更需要使集以表的形式表现。尽量少地选取特定的类型(诸如IEnumerable, ICollection此类)以保证你的方法效率的最大化。
4.泛型
泛型是一种在定义独立类型结构体与设计算法上一种十分有力的工具,它可以强制类型变得安全。
用像List这样的泛型集来替代数组列表这种无类型集,既可以提升安全性,又可以提升性能。
在使用泛型时,我们可以用关键词“default”来为类型获取缺省值(这些缺省值不可以硬编码写进implementation)。特别要指出的是,数字类型的缺省值是o,引用类型与空类型的缺省值为null。
Tt =default(T);
5.类型转换
类型转换有两种模式。其一显式转换必须由开发者调用,另一隐式转换是基于环境下应用于编译器的。
常量o可由隐式转换至枚举型数据。当你尝试调用含有数字的方法时,可以将这些数据转换成枚举类型。
转换通常意味着以下两件事之一:
1.RuntimeType的表现可比编译器所表现出来的特殊的多,Cast转换命令编译器将这种表达视为一种更特殊的类型。如果你的设想不正确的话,那么编译器会向你输出一个异常。例如:将对象转换成串。
2.有一种完全不同的类型的值,与Expression的值有关。Cast命令编译器生成代码去与该值相关联,或者是在没有值的情况下报出一个异常。例如:将double类型转换成int类型。
以上两种类型的Cast都有着风险。第一种Cast向我们提出了一个问题:“为什么开发者能很清楚地知道问题,而编译器为什么不能?”如果你处于这个情况当中,你可以去尝试改变程序让编译器能够顺利地推理出正确的类型。如果你认为一个对象的runtime type是比compile time type还要特殊的类型,你就可以用“as”或者“is”操作。
第二种cast也提出了一个问题:“为什么不在第一步就对目标数据类型进行操作?”如果你需要int类型的结果,那么用int会比double更有意义一些。
获取额外的信息请参阅:
http://blogs.msdn.com/b/ericlippert/archive/tags/cast+operator/
在某些情况下显式转换是一种正确的选择,它可以提高代码可阅读性与debug能力,还可以在采用合适的操作的情况下提高测试能力。
6.异常
异常并不是condition
异常不应该常出现在程序流程中。它们代表着开发者所不愿看到的运行环境,而这些很可能无法修复。如果你期望得到一个可控制的环境,那么主动去检查环境会比等待问题的出现要好得多。
利用TryParse()方法可以很方便地将格式化的串转换成数字。不论是否解析成功,它都会返回一个布尔型结果,这要比单纯返回异常要好很多。
注意使用exception handling scope
写代码时注意catch与finally块的使用。由于这些不希望得到的异常,控制可能进入这些块中。那些你期望的已执行的代码可能会由于异常而跳过。如:
Frobber originalFrobber =null;try{ originalFrobber =this.GetCurrentFrobber();this.UseTemporaryFrobber();this.frobSomeBlobs();}finally{this.ResetFrobber(originalFrobber);}
如果GetCurrentFrobber()报出了一个异常,那么当finally blocks被执行时originalFrobber的值仍然为空。如果GetCurrentFrobber不能被扔掉,那么为什么其内部是一个try block?
明智地处理异常
要注意有针对性地处理你的目标异常,并且只去处理目标代码当中的异常部分。尽量不要去处理所有异常,或者是根类异常,除非你的目的是记录并重新处理这些异常。某些异常会使应用处于一种接近崩溃的状态,但这也比无法修复要好得多。有些试图修复代码的操作可能会误使情况变得更糟糕。
关于致命的异常都有一些细微的差异,特别是注重finally blocks的执行,可以影响到异常的安全与调试。更多信息请参阅:
http://incrediblejourneysintotheknown.blogspot.com/2009/02/fatal-exceptions-and-why-vbnet-has.html
使用一款顶级的异常处理器去安全地处理异常情况,并且会将debug的一些问题信息暴露出来。使用catch块会比较安全地定位那些特殊的情况,从而安全地解决这些问题,再将一些问题留给顶级的异常处理器去解决。
如果你发现了一个异常,请做些什么去解决它,而不要去将这个问题搁置。搁置只会使问题更加复杂,更难以解决。
将异常包含至一个自定义异常中,对面向公共API的代码特别有用。异常是可视界面方法的一部分,它也被参数与返回值所控制。但这种扩散了很多异常的方法对于代码的鲁棒性与可维护性的解决来说十分麻烦。
抛出(Throw)与继续抛出(ReThrow)异常
如果你希望在更高层次上解决caught异常,那么就维持原异常状态,并且栈就是一个很好的debug方法。但需要注意维持好debug与安全考虑的平衡。
好的选择包括简单地将异常继续抛出:
Throw;
或者将异常视为内部异常重新抛出:
抛出一个新CustomException;
不要显式重新抛出类似于这样的caught异常:
Throw e;
这样的话会将异常的处理恢复至初始状态,并且阻碍debug。
有些异常发生于你代码的运行环境之外。与其使用caught块,你可能更需要向目标当中添加如ThreadException或UnhandledException之类的处理器。例如,Windows窗体异常并不是出现于窗体处理线程环境当中的。
原子性(数据完整性)
千万不要让异常影响到你数据模型的完整性。你需要保证你的对象处于比较稳定的状态当中——这样一来任何由类的执行的操作都不会出现违例。否则,通过“恢复”这一手段会使你的代码变得更加让人不解,也容易造成进一步的损坏。
考虑几种修改私有域顺序的方法。如果在修改顺序的过程当中出现了异常,那么你的对象可能并不处于非法状态下。尝试在实际更新域之前去得到新的值,这样你就可以在异常安全管理下,正常地更新你的域。
对特定类型的值——包括布尔型,32bit或者更小的数据类型与引用型——进行可变量的分配,确保可以是原子型。没有什么保障是给一些大型数据(double,long,decimal)使用的。可以多考虑这个:在共享多线程的变量时,多使用lock statements。
7.事件
事件与委托共同提供了一种关于类的方法,这种方法在有特殊的事情发生时向用户进行提醒。委托事件的值在事件发生时应被调用。事件就像是委托类型的域,当对象生成时,其自动初始化为null。
事件也像值为“组播”的域。这也就是说,一种委托可以依次调用其他委托。你可以将一个委托分配给一个事件,你也可以通过类似-=于+=这样的操作来控制事件。
注意资源竞争
如果一个事件被多个线程所共享,另一个线程就有可能在你检查是否为null之后,在调用其之前而清除所有的用户信息——并抛出一个NullReferenceException。
对于此类问题的标准解决方法是创建一个该事件的副本,用于测试与调用。你仍然需要注意的是,如果委托没有被正确调用的话,那么在其他线程里被移除的用户仍然可以继续操作。你也可以用某种方法将操作按顺序锁定,以避免一些问题。
publiceventEventHandler SomethingHappened;privatevoidOnSomethingHappened(){// The event is null until somebody hooks up to it// Create our own copy of the event to protect against another thread removing our subscribersEventHandler handler = SomethingHappened;if(handler !=null) handler(this,newEventArgs());}
领取专属 10元无门槛券
私享最新 技术干货