LinuxAid.com.cn 01-07-10 10:51 175p axing
使用断言
断言是一个在假设不正确时会大声抗议的函数或宏指令。可以使用断言来验证在程序中作出的假设并排除意外情况。一个断言函数往往大致带有两个内容:假设为真时的布尔表达式和一个为假时要打印出来的信息。以下是一个假定变量Denominator不为零时一个Pascal断言:
Assert ( Denominator<>0,'Denominator is unexpectedlg equal to 0.' );
这个断言假定Denominator 不等于”0”,第一部分Denominator<>0 是一个布尔表达式,其结果为True或False。第二部分是当第一部分的结果为False肘,将要打印出来的信息。即使不愿让用户在最终软件中看到断言信息,在开发和维护阶段,使用断言还是非常方便的。在开发阶段,断言可以消除相互矛盾的假设,消除传入于程序的不良数值等等。在维护,可以表明改动是否影响到了程序其它部分。
事实上,断言这项技术已经是在各处被使用了,在Windows平台的Visual C++的环境下已经为开发者预先做好了很多的断言,做为一个开发者所要做的事情仅仅只需要去调用它而已;当然,Linux下的开发者就没有这么幸运,但是如果你要使用他的话并不是一件困难的事情,做为范例,你可以在Linux的源码中找到相关的资料,相关的资料还包括了下文将要提到的预处理等技术。
断言过程是非常容易写的,下面就是一个用Pascal写成的例子:
Procedure Assert
(
Aseertionn: boolean;
Message : string
);
begin
if( not Assertion)
begin
writeln(Messase);
writeln('stopping the program.');
halt(FATAL ERROR)
end
end;
一旦写好了这样一个过程,就可以用像第一个例子那样的语句来调用它。
下面是使用断言的一些指导方针:
如果有预处理程序的话,使用预处理程序宏指令。如果在开发阶段使用预处理程序处理断言,那么在最终代码中把断言去掉是非常容易的。
在断言中应避免使用可执行代码,把可执行代码放入断言,在关闭断言时,编译程序有可能把断言捎去。请看以下断言;
ASsert(FileOpen(InputFile)<>NULL,’Coulldnt Oped input file’);
这行代码产生的问题是,如果不对断言进行编译,也编译不了打开文件的代码,应把可执行语句放在自己的位置上,把结果赋给一个状态变量,然后再测试状态。以下是一个安全使用断言的例子:
FileStatus : FileOpen (InputFile);
Assert(FileStatus<> NULL,'couldn't Opeh input file');
事实上,断言最大的用处就是产生两种不同的软件版本:调试版和最终版。我不知道大家是怎样调试程序的,但是在自己的程序中加入标准的断言程序,和预处理程序(例如:#define DEBUG)配合使用是一个不错的主意。如果用过MS的Visual C++的开发者都知道(又用讨厌的Bill做例子实在是违心呀,不过仅仅用于学术用途)软件有Debug和Release两种版本,其中就有这方面的区别。
另外再插一句,在Window平台下开发软件真是很舒服,又有IDE环境又有版本控制的,特别方便,Linux下就比较辛苦一点了,不过Linux下至少还有很多源码可以参考,而我所从事的在大型机环境下的开发工作就更是只能用惨字来形容了,什么东西都要自己写,那个时候打心眼儿里觉得提出软件复用的人是天才!
计划去掉调试帮助
调试帮助措施包括:断言、内存检查报告、打印语句等及其它一些为方便调试而编写的代码。如果所开发的软件是供自己使用的,那么把它们保留在程序中并无大碍。但是,如果是一个商用软件,那么这些措施留在程序中,则会影响其速度和占用空间等性能指标。在这种情况下,应事先作好计划,避免调试信息混在程序中,下面是几种方法。
使用版本控制。版本控制工具可以从同一源文件中开发出不同版本的软件。在开发阶段,可以设置包含全部调试辅助手段的版本控制工具,这样,到了产品阶段,就可以很容易地去掉在商用版本中所不希望出现的这些辅助手段。
小弟我才疏学浅,并不知道Linux下有什么好用(强大、免费)的版本控制工具,据说Rational公司的CleanCase有Linux的版本,只是我从来没有见过。至于Windows平台上就有一些,MS的SourceSafe就很不错哟,也比较好找,如果你要开发Linux下的程序的话,可以考虑使用它来帮助你管理版本,不知道Bill会不会气死。
使用内部预处理程序。如果在编程环境中带有预处理程序,如C 语言,那么仅用一下编译程序开关,就可以加入或去掉这些辅助手段。可以直接使用预处理程序,也可以通过编写宏指令来进行预处理程序定义。下面是一个用c语言写成的,直接使用预处理程序的例子:
#define DEBUG
…
#ifdefined(DEBUG)
/*调试代码*/
…
#endif
这种思想可能有几种表现形式。不仅仅是定义DEBUG,还可以赋给它一个值,然后再测试它的值,而不是测试它是否被定义了。用这种方法可以区分调试代码的不同层次。也可能希望某些调试用代码长期驻存在程序中,这时可以使用诸如#if DEBUG>0 之类的语句,把这段代码围起来,其它一些调试代码可能是有某些专门用途的,这时可以用#if DEBUG—一PRINTER ERROR 这样的语句把这段代码围起来,在其它地方,还可能想设置调试层次,可用如下语句:
#if DEBUEG>LEVEL_A
要是不喜欢程序内部充斥着#if defined()这样的语句的话,可以用一个预处理程序宏指令来完成同一任务。以下是一个例子:
#define DEBUG
#if defined( DEBUG)
#define DebugCode( code fragment){code_fragment}
#else
#define DebugCode(code fragment)
#endif
DebugCode
(
statement 1;
statement 2;
…
statment n;
)
与使用预处理程序的第一个例子一样,这种技术也有多种形式,可以使它更复杂一些,从而不是简单地全部包括或全部排除调试代码。
编写自己的预处理程序。如果编程语言中没有预处理程序,可以自己编写一个加入或去掉调试代码的预处理程序,这项工作是非常容易的。还要建立一个标识调试代码的约定,并编写自己的预编译程序来遵循这一约定。比如,在Pascal 中,可以编写一个对如下关键字作出反应的预编译程序:/#BEGIN DEBUG/和/#END DEBUG/人并写一个批处理文件来调用这个预处理程序,然后再编译这段已经预处理过的代码。从长远观点来看,这样做可以节约许多时间,因为你不会误编译没有预处理过的代码。
保留使用调试程序。在许多情况下,可以调用一个调试子程序来进行调试工作。开发阶段根据DEBUG是否定义,包含或不包含这段代码。在控制返回调用程序之前,这个子程序可能进行几项操作。在最终软件中,可以用一个子程序来代替那个复杂的调试子程序,这个子程序将立即返回控制,或者在进行两个快速操作之后,返回控制。使用这种方法,对性能带来的影响很小,与编写自己的预处理程序来相比,其速度也要快得多。所以,有必要保留这个子程序在开发阶段和最终产品阶段的两个版本,以供在将来的开发和产品调试中使用。
比如,可以用一个检查传入其中指针的子程序作为开始:
Procedure DoSomething
(
Pointer:PONITER_TYPE;
...
);
begin
{ check parameters passed in }
CheckPointer(Pointer); 这行调用检查指针的子程序
...
end.
在开发阶段,CheckPointer()子程序将对指针作全面检查,这项工作可能很费时,但却非常有效,它很可能是以下这个样子的:Procedure CheckPointer( Pointer: POINTER_TYPE);——这个程序检查传入的每个指针,它可以在开发期间使用.完成格外检查:
begin
{perform check 1--maybe chech that it's not nil}
{perfom check 3 --maybe check that what is point to isn't corrupted}
{perform check n--...}
end;
当程序进入最终产品阶段时,可能并不希望所有的内务操作都与指针检查联系到一起,这时,可以用下面这个子程序来代替刚才那个子程序:Procedure CheckPointer (Pointer: POlNTER_TYPE ); 本程序仅是即返回其调用程序
begin
{no code;just return to caller}
end;
以上这些并不是去掉调试辅助工具的所有方案,但从提供一些在你的环境下能有效工作守案这个思路的角度来说,这些已经是足够的了。
预计改动
改动几乎是每个程序都不可避免的现象。比如,开发一个旧的软件新版本,就需要对原有代码作出许多改动,不过,即使是在开发一个软件的第一版时,也不得不由于加入某些没有预计到的功能而对其进行改动。在开发软件时,应该努力作到使它很容易地进行改动。而且,越是可能的改动,越是要容易进行,把你在其中预想到的改动域隐含起来,是减少由于改动而对程序带来影响的最有力武器之一。
预计改动这种东西是说起来容易做起来难的啦,软件的需求是不断的改变的,想一劳永逸的实现软件功能无疑是痴人说梦,但是你如果按照在以前的文章里提到的耦合的方法去做的话,在用户需求改变的时候你就会有惊喜的收获。
输入垃圾不一定输出垃圾
一个好的程序从来不会输出乱七八糟像垃圾似的东西,不管它被输入的是什么。一个好程序的特点是“输入垃圾,什么也不产生”,或“输入垃圾,输出错误信息”,也可以是“不允许垃圾进入”。从现在的观点来看“输入垃圾,输出垃圾”,往往是劣质程序。
检查所有外部程序输入的数值。当从用户文件中读取数据时,要确保读人的数据值在允许范围之内。要保证数值是可以取的,并且字符串要足够短,以便处理。要在代码中注释出输入数据的允许范围。
检查全部子程序输入参数值。检查子程序输入参数值,事实上与检查外部程序数据是一样的,只不过此时由子程序代替了文件或用户。
以下是一个检查其输入参数的子程序的例子,用c语言编写:
float tan
(
float OppositeLength;
float AdjacentLength;
)
{
/*计算角度正切*/
Aseert( AdjancentLength != 0,”AdjanceLength deteced to be 0." );
return (OppsiteLenght/AdjancetLength);
}
决定如何处理非法参数。一旦发现了一个非法参数,该如何处理呢?根据不同情况,可以希望返回一个错误代码、返回一个中间值、用下一个合法数据来代替它并按计划继续执行、与上次一样返回一个正确答案、使用最接近的合法值、调用一个处理错误的子程序、从一个子程序中调用错误信息并打印出来或者干脆关闭程序。由于有这样多的方案可供选择,所以当在程序的任一个部分处理非法参数时,一定要仔细,确定处理非法参数的通用办法,是由结构设计决定的,应该在结构设计层次上予以说明。
异常情况处理
应核预先设计好异常处理措施来注意意想不到的情况。异常处理措施应该能使意外情况的出现在开发阶段变得非常明显,而在运行阶段又是可以修复的,例如,在某种情况下使用了一个case 语句,其中只预计到了五种情况,在开发阶段,应该能利用异常情况处理产生一个警告,提示出现了另外一种情况。而在产品阶段,应该利用异常情况处理做一些更完美的工作,比如向一个错误记录文件中写入信息等。总之,应该设计出不必费多大周折,就可以从开发阶段进入产品阶段的程序。
上和大家聊了这么多东东,其实实际用的时候未必会有很大的作用的(啊,谁打我),大家别生气(耍我!我还打!),大家想想“独孤九剑”,写程序也是一样的,关键在于你怎么用,只是生搬硬套的话是成不了令狐冲的啦。其中最关键的还是从思想的角度来认识程序写作的问题。小弟有个同学就是这样子的了,写程序已经到了收发由心的地步啦。
其实有关程序写作的东西还有很多,不过我想先缓一下,在接下来的几期中,我想和大家聊一聊软件设计方面的一些事情,让大家在比较高的层次上对软件这东西有个了解。