三.编写优质无错代码的经验 在说了上面很多理论性的问题之后,来看一看具体问题。先来看一看一个具体的题目:(我本人就是先在网上看了这个题目,才对这个讨论会发生兴趣的) 题目1: 作为开发团队的一员,你需要实现一些库函数提供给其他人使用。假设你实现的一个函数原型如下: int DoSomeThing(char* pParam) { ... } 你们约定好参数pParam不能为NULL,但为了防止调用者错误传递NULL,你需要在你的函数里做判断处理。 请问你会选择那种方式,并说明原因? (a) if (!pParam) return 0; (b) if (!pParam) return ERROR_PARAM; (c) if (!pParam) pParam = ""; ... (d) if (!pParam) throw EXCEPTION_ERROR_PARAM; (e) if (!pParam) MessageBox(...); (f) assert(!pParam); (附加说明一点,基于目前开发人员技术分布情况和参与讨论会的人员的技术分布情况,这次所列举的例子都是C/C++和Java方面的,不涉及到VB, PB, Delphi等语言。不过对于这些程序员,讨论也是有借鉴作用的。)关于这个问题,大概是所有的程序员都会遇到的。所以,在网上和讨论会上,都发生了激烈的争论和意见交换。我大概把主要的几种观点记录了一下,列举在下面: 1、选择f的理由 因为非NULL是约定,所以可以确定是调用者的问题,f可以明确地指出这一点,防止错误扩散。 我的附加说明: 防止错误扩散的意思是,如果用其他方式,比如throw exception的方式,这个异常不一定会在调用此函数的上一层被捕捉到,可能会被继续抛出直到最上一层或者直到在某一层被catch到,这样的话,错误就会距离发生地点很远,扩散开来。这一观点,代表了一大部分的程序员的观点。 2、反对用f 不赞成assert, assert更重要的作用是程序体里面的一个注释, 在阅读程序的时候起作用不能依赖他来检测错误, 很大程度上assert容易使使用者依赖它本不应该依赖的东西。 这也代表了部分程序员的观点,认为assert是不可依赖的,而应该依赖于错误检测,比如返回值或者异常。 3、另外一种观点 f和d都可取。如果没有系统开销的考虑,d则更好些。可以一举两得。如果没人catch这个exception,其结果就跟f一样,按bug处理,dump core留下一stack trace。如果有人catch,那就按运行错误处理......但是返回一特初值表示错误,只是将错误上交,掩耳盗铃而已。最终总得有个人assert,messagebox,throw exception,perror+exit,或别得什么的。既然已经是约定,就干脆付起责任。 4、一种反对d的理由 不可用d, 这就像你用人,却不相信人一样,偏要try,catch防范他。其实那个错是自己造成的,如果看到异常就容易不检讨自己。 5、关于观点3的支持意见 讨论过程中,有人认为assert检查的是bug, 而异常是可以恢复的意外情况。所以,观点3的支持者说:可恢复的意外是可以理解的,但可恢复的bug就没什么意义了。既然已经约定好了,你再违背,就属于是bug而不是意外了(比如打不开文件什么的)。很多库函数都不检查指针的合法性(除了系统调用以外,因为总不能让系统dump core吧),也不检查指针是否为NULL(因为如果层层都检查,必定劳民伤财,干脆让最上面调用的人在调用之外查)。 6、选择d+f 选f+d, 好处如下: a以最激烈的方式,充分暴露调用都的错误!能及时修改BUG b便于调试,问题出现后,直接到事故现场。比120还快! c对于realse版的代码没有任何副作用。 d以处理的代价来看 采用断言也是编写最小一种。 e它是多语种,多平台所通用的方式, 如:C /C++ VB,Java1.4 在win ,unix通吃, 便于移置! 如果在现实中,测试没有能找到所有的BUG,那可能就要用异常来帮忙了! 当然,我也提出了我的观点, 我支持观点6。理由如下: assert只在debug标志的时候有用,而在编译release版本的时候不起作用。assert对于检查硬编码的错误,是非常有用的,能够及时的查处编码的错误。比如borland c++的类库源代码中就有很多这样的assert。但是assert 不是万能的,因为有很多错误的发生不是完全在编译时发生的,而是运行时的错误。在release后,assert是不可能依赖的。那么,我们就需要exception这一机制来检测运行时错误,并相应的做出处理。当然,在异常检测和处理过程中还有许多需要讨论的问题,由于不是这一题目的范围,我们没有必要继续讨论得太多,但是,提出来希望大家注意:异常不是捕获了就完成任务了,而要对于不同的情况,采取不同的处理办法,千万不能只是捕获,而不做任何处理,那样和不捕获异常没有任何区别。 在题目刚刚提出的时候,选择各种答案的人都有,所以,我有必要在这里把其他答案为什么不能选的理由说一下。 (a) if (!pParam) return 0; 这是很多初级程序员常常采取的一种方式。返回值设为0。 因为函数的返回值往往是计算的结果,不赞成把错误标志值和计算结果混在一起使用,容易造成使用者的误会。当然,在很多unix函数中,由于历史原因,还存在很多这样子的函数,所以需要指出,不要沿用这种方式。 (b) if (!pParam) return ERROR_PARAM; b比a稍微好一点点,返回了一个常量或者预定义的宏。 从返回值的字面上,调用者能知道发生了什么错误,但是,这也不是一种好的方法。 (c) if (!pParam) pParam = ""; ... 这是最不好的方式。直接给pParam赋予空字符串,然后继续函数过程,这容易造成不可预料的后果,是程序不稳定的根源。 (d) if (!pParam) throw EXCEPTION_ERROR_PARAM; 抛出异常,刚刚已经讨论过了,不再赘述。 (e) if (!pParam) MessageBox(...); 这是一种比较可笑的方式,当然也有不少人用。MessageBox是直接弹出一个对话框,告诉使用者,出错了。但是并不做任何处理,程序继续往下执行,直到出错崩溃。呵呵 (f) assert(!pParam); 断言,刚刚已经讨论过了,不再赘述。 以上这个题目,引发了所有与会者的兴趣,讨论异常热烈,最后,主持人也给出了自己的观点:d+f。当然这并不是标准答案,因为编程这一门课程本来就没有什么标准答案,大家见仁见智,这个答案只是经验的积累。 主持人紧接着列出了"编写优质无错代码的经验": a.理想的编译器和实际的编译器 b.使用断言 c.函数的界面设计 d.考虑风险 e.态度的问题 以上是本节的主要内容。断言,刚刚的问题中已经讨论过了,来看看其他的内容。
|