程序设计风格杂谈
任甲林
程序设计语言首先是人与计算机之间进行交流的表达方式。由于人们定义了汇编语言,编译程序等保证程序对计算机的可理解性,因而计算机可以机械地、被动地理解并执行程序。 程序设计语言还是人与人之间进行交流的工具。单纯的作为人机交流的工具,只要程序能够正确地忠实地表达设计者的思想,也就发挥了其作用,但是人与人之间的交流没有一种固定的统一的模式,因此作为人与人之间的交流工具,还要表达的清晰易懂,能够为其他程序员所理解。 这也正式要求程序员讲究程序设计风格的主要原因。
有人讲程序设计是一门个人艺术,他饱含了程序员个人的创造性,正是这样,才使得很多程序构思精巧,耐人寻味。但是同时它却使得程序的可读性较差,尤其是在多个人合作开发一个软件时,风格迥异的程序使得软件的可靠性与可维护性大大降低。
为了确保程序的易读性,保证软件质量,降低开发成本,1969年荷兰学者Dijkstra等人提出了"结构化程序设计方法"。 他们规定了程序结构的三种基本构件,即:顺序结构,选择结构,循环结构,每种结构严格的只有一个入口和出口,任何其他的程序结构均可由这三种基本结构构造出来。该方法还强调采用自顶向下的分层次设计方法,对任务进行微划分。事实上结构化程序设计方法确实很有实效,采用该方法编写出来的程序,结构良好,易于阅读,易于验证。
1.1 命名的风格
程序、变量、函数、过程、类等的名字象人的名字一样是一个记号,是用来标志这个对象的,一个好的命名应该简单、易记、易于理解。
让我们来看一个极端的命名方法:
求100以内的素数之和
a=0
for (aa=2;aa<100;aa+1)
{
aaa=true;
for (aaaa=2;aaaa<aa,aaaa+1)
{
if mod(aa/aaaa)!=0 then{
aaa=false;
break;}
}
if aaa a=a+aaaa;
}
这是一个未受过编程训练的新手写的程序,在此程序中出现了4个变量,他分别用a,aa,aaa,aaaa为此4个变量命名,程序的可读性很差,读完之后很疲惫啊,如果在一段程序里有20个变量,按这种方法去设计程序,那可就糟透了。
. 使用能够表达过程(函数)所完成的逻辑功能或变量的作用、数据结构的作用的名字.
例如:
给判定两个数中最大数的函数命名为MAX,给表示光标位置的变量命名为: CURSOR_ROW,CURSOR_COL等。
. 要尽量避免各名字视觉上的相似,各名字要有两个或两个以上的字符不同,以造成适当的心理上的距离.
. 避免使用拼法相近的字母区分符号名,例如:
b与6 M与N U与V
g与8 O与0 Z与2
J与I,T q与g I与1
T与7 s与5 Q与2
. 使用前后一致的缩写格式,以缩短变量名
例如:
对于长的变量名均去掉其元音字母:
WINDOW_TITLE_COLOR: WNDW_TTL_CLR
. 不要使用语言中的关键字,以免产生歧义。
. 尽量以意义命名而不以读音命名.
例如:
以英文含义命名,而不是采用汉语拼音的第一个字母命名。 试比较一下下面的两个命名哪个更容易猜出其含义: CURSOR_ROW, GBHZB
. 在程序中尽量少出现常数值,最好以能代表其含义的变量或宏代替。
. 在同一个软件中对符号名的命名方法要一致。
1.2 注释的风格
对程序的注释可分为以下三种:
. 序言性注释:
程序名称 作者
创建日期 最近一次修改日期
功能概述 输入数据或入口参数
输出数据或出口参数 主要处理流程或主要算法描述
主要的数据结构 利用的子程序
相关的全程变量 局部变量的含义
调用时注意的问题
. 程序块注释:
数据结构 算法或处理流程
注意问题
. 指令级注释:
语句功能 实现技巧
变量含义
上述三种注释的重要性依次递降,侧重于序言性注释和程序块注释。
.注释要和内容相匹配
当对源码修改时,千万不要忘记更新注释,一个与内容不相符的注释,只能对阅读者起到误导的作用。
.注释不要仅仅重复语句。
.不要对糟糕的程序进行注释,重新编写。
注释对软件的可理解性只能起辅助作用,如果一段程序写的难以理解、算法混乱,试图通过增加注释来改善这种局面正如给一只乌鸦披红挂绿,它仍是乌鸦。
.对全局变量的作用进行注释。
.对循环结构或选择结构进行注释。
1.3 书写格式
采用缩进格式体现程序的控制结构。
采用空行、空格来增加程序的清晰性。
不要将多条语句写在一行内。
尽量使一行语句的长度小于屏幕的宽度或小于打印机一行的宽度。
使用大小写或下划线等增加程序的可读性。
1.4 控制结构的风格
■ 不要破坏三种基本的控制结构.
在1966年,Bohm和Jacopini证明了在程序设计语言中,只要由顺序,选择,循环三种控制结构就足以表示出其它各种复杂的程序控制结构。在结构化的程序设计语言中,这三种结构都易于实现。这里要强调的是,如果某种语言提供了GOTO语句,千万不要让其破外了这三种基本结构!
请看下面的三个程序流程示意图,显然它们都是非结构化的!
┏━━━━━━┓ ↓
↓ ┃ ↓ ↓ ┏━━━┻━┓
┏━━┻┓┃ ┏━━┻━┓ ┗┳━━━┯┛
┏━┓ ┗┳┯━┛┃ ┗┳┳━━┛ ↓━┛┏━↓↓
┃ ↓ ↓━━┛│ ┃ ↓━━┛┃ ┏━━┻━┓┃┏━┻┓
┃┏┻━┻━┓ │ ┃┏━━┻━┓ ┃ ┗━┳━┳┛┃┗┳━┛
┃┗━━━┳┛ │ ┃┗┳━━┳┛ ┃ ↓ ┗━┛ ↓
┃ ↓ ↓ ┗━┛ ↓ ↓ ┏━━━━━━┓
┃ ┏━┻━━┻━┓ ┏━┻━━┻━┓ ┗━━┳━━━┛
┃ ┗┳━┳━━━┛ ┗━━┳━━━┛ ↓
┗━━━┛ ↓ ↓
如果不采用GOTO语句,上面的三个流程是无法实现的。事实上在设计算法的时候往往犯这样的错误,尤其在采用程序流程图作为详细设计工具时。如何避免这种错误呢?
.采用N-S图或PAD图来表达算法。它们强制你利用结构化的方法来设计数据流程,否则无法画出N-S图或PAD图。
.尽管采用GOTO语句并不一定是非结构化的,但您最好不要采用GOTO语句。
■循环结构的风格
.不要在循环体内跳出,也不要由循环体外跳入。
在循环体内跳出或由循环体外跳入都破坏了结构化构件的仅有一个入口一个出口的原则。在一个循环结构中,下面的结论是正确的:
•经过该结构入口的所有路径都要经过该结构的出口;
•所有可到达该结构出口的路径都要经过该结构的入口;
┏━━━┓┃
┃ ↓↓
┃ ┏━┻┻━━┓ 入口
┃ ┗━━┳━━┛
┃ ↓
┃ ┏━━┻━━┓
┃ ┗━━┳━━┛
┃ ↓
┃ ┏━━┻━━┓ 出口
┃ ┗┳━┳━━┛
┗━━┛ ↓
. 减少循环的嵌套次数,最好不要超过5层。
根据心理学的结论,人们同时可以把握的对象在7个左右,即7加减2定律,超过9层的循环或嵌套难以理解。
. 避免死循环.
死循环现象往往在循环体内非规律性的改变循环终止的条件时发生。
例如:
在循环体内改变FOR NEXT结构的循环变量,在一些语言中是允许的,此时千万要小心。
虽然有时可采用假死循环的结构来进行程序设计,但一定要注意退出条件,千万不要弄假成真。如:
采用计算机模拟某人一生的健康状况,产生随机数0,1,...,20来模拟其在某一岁时的身体状况,为0代表死亡
I=1
DO WHILE .T.
GOOD_NUM=RANDOM(20)
IF GOOD_NUM=0
EXIT
ELSE
? I,GOOD_NUM
ENDIF
I=I+1
ENDDO
如果产生的随机数大于0,则此程序不可能终止,显然人不可能长生不老的。
.减少循环体内的语句,降低循环结构的复杂性。
循环结构复杂性>=选择结构复杂性>顺序结构复杂性
■选择结构的风格.
. 尽可能在判断语句之后紧接着写上相应的操作。
例:
求三数中的最大数
方法一:
IF A>B
IF B>C
MAX=A
ELSE
IF A>C
MAX=A
ELSE
MAX=C
ENDIF
ENDIF
ELSE
IF B>C
MAX=B
ELSE
MAX=C
ENDIF
ENDIF
方法二:
IF A>B .AND. A>C
MAX=A
ELSE
IF B>C .AND. B>A
MAX=B
ELSE
MAX=C
ENDIF
ENDIF
例 判断某1年是否是闰年
计算某一年是否是闰年的正确方法是:
(1)可以被4整除的年是闰年;
(2)但是,可以被100整除的年不是闰年;
(3)但是,可以被400整除的年是闰年。
方法一:
if (mod(a/400)==0)
is_run=true;
else
if (mod(a/100)==0
is_run=false;
else
if (mod(a/4)==0)
is_run=true;
else
is_run=false
方法二
if (mod(a/4)==0)
if (mod(a/100)==0)
{if (mod(a/400)==0)
is_run=true;
else
is_run=false;
}
else
is_run=true;
else
is_run=false;
. 减少选择结构嵌套次数。
就象减少循环嵌套的层数一样,选择结构的嵌套次数也一定要减少。可采用如下的技术:
采用多重分支结构(DO CASE语句或SWITCH语句等)代替多重判断;利用数组避免重复的控制流等。
. 冗余检查
在程序中加入冗余检查或冗余语句,是增强程序的可靠性,消除隐藏错误的一种有效手段,尤其在程序中变量较多的情况下。当确信在某时程序的状态应为某状态时,可通过强行赋值将程序的状态置位。 此时要在注释中说明。
. 不要在条件外转入,也不要在条件内转出。
1.5 控制复杂度的风格
■尽量使用标准库函数,过程,子例程
试比较一下,下面的两种方法哪一种更好:
问题: 求三数中的最大数
方法一
IF A>B .AND. A>C
MAX=A
ELSE
IF B>C .AND. B>A
MAX=B
ELSE
MAX=C
ENDIF
ENDIF
方法二
MAX_NUM=MAX(MAX(A,B),C)
事实上有些程序员具有这种思想,但是由于对语言的不熟悉,不晓得所使用的语言中有相应的函数,而走了弯路,对这种情况,下面的方法或许有用:
(1). 在学习一种新语言时,先通读有关的标准函数,过程,类等,从整体上有个印象,当需要时可凭记忆查询资料。
(2). 类比。 与以前熟悉的语言进行类比。
(3). 询问他人,互通有无。
■用函数代替重复的表达式或程序段
对于在程序中多次重复的表达式或程序段,最好采用函数或宏定义来替代,以增加程序的可读性,阅读有意义的函数名或宏名称可以很快的把握到程序的作用。
此时在修改程序时要注意到其负效应。
■以参数传递数据而不通过全程变量来传递数据,以传值方式而少用传地址方式进行参数传递。
有些程序员习惯于定义很多全程变量,在各个子模块间通过全程变量进行数据共享,这种风格是程序员疏于思考的表现。全程变量越多,程序越复杂,隐患也越多。在对全局变量命名时要在标识符上有所区分。
以传值方式传递参数比较直观、简单、易于理解,传地址方式使子模块的影响扩散到了父模块,增加了阅读、调试程序的难度。
■减少过程参数的数量,可以减少调用的复杂性,使接口易于控制。
通用性子程序往往参数较多,可采用层次模块法解决之:
A
B
C
C1
C2
B1
D
D1
D2
B2
如上所示:
A 为最底层的通用子称序,假定含有10多个参数;
B B1 B2 为对A的参数进行默认,取不同的参数进行默认,以进行减少需传递的参数值;
C C1 C2 D1 D2 D分别为调用B B1的应用程序。
■慎用递归技术
递归技术是表达算法的一种有效方法,他使算法看上去相当简炼,而且符合人的思维习惯,但是对于递归方法无法能够准确地预测出其对堆栈空间的需求,而且他增加了系统开销,运行速度慢,对于复杂的问题采用递归方法往往使聪明的程序员也陷入困境。
.在不是很困难便能采用循环替代递归的地方,尽量采用循环。
.严防无限层次的递归.
.在使用递归时,对堆栈空间进行充分的考虑,防止堆栈溢出。
1.6 保证可靠性的风格
■检查输入数据的合理性与合法性,防止垃圾进垃圾出
例:
读入三个数分别表示一个三角形的三边,计算其面积。如果录入的三个数组不成三角形。
读入一年轻人的年龄。如果录入的年龄大于五十岁显然是不合理的。
读入一值作为某数组的下标 如果录入的数为负数或过大都是不合理的。
■非关键性算法要首先追求算法的清晰性。
例如:
在C语言中,对一个整数矩阵赋值,其对角元为1,其他值为0
方法一:
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
a[i,j]=(i/j)*(j/i);
方法二:
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
if (i==j)
a[i,j]=1;
else
a[i,j]=0;
方法三:
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
a[i,j]=(i==j) ? 1 : 0;
在方法一中利用了C语言中自动的强制类型转换:
当i>j时,j/i为0,a[i,j]=0
当i<j时,i/j为0, a[i,j]=0
当i=j时,j/i为1,j/i也为1,a[i,j]=0
构思相当巧妙,俨然是出自一名熟练的程序员之手,但是与方法二,方法三相比,未免有故弄玄虚之嫌。
■尽量避免使用临时变量
如果一个临时变量在赋值后,仅被使用了一次,那么最好去掉它。
例如:
计算表达式:(X-X^2)^2+(1.0-X^2)^2的值。
f1=x-x*x
f2=1.0-x*x
fx=f1*f2+f2*f2
去掉临时变量:
fx=(x-x*x)^2+(1.0-x*x)^2
如果一个临时变量能够增加程序的可读性或不设该变量无法完成任务,则保留它。
例如:
交换A,B两个数值型变量的值
temp=a
a=b
b=temp
如果采用下面的小技巧,虽然减少了变量的个数,但往往在读到该处时,要停顿一下:
a=a+b
b=a-b
a=a-b
两种方法相比较,优劣显然。
上面的例子还说明:
不要热衷于小技巧! 小心弄巧成拙!
■避免有二义性的表达式,可通过加括号等方法改进之。
例:
A+B/2*C
IF A .AND.B .OR. B .AND. C THEN ......
改为:
(A+B/2)*C
IF (A .AND. B.) .OR. (B .AND. C) THEN ......
■控制模块的长度,最好不要超过200行。
短小的程序易于阅读,控制流程在程序中的跨度小。
1.7 其他风格
■克服急于编程的恶习,要采用详细设计工具将思想表达清楚后再去编程。
急于编程的恶习似乎每个程序员都有这样的经历。 在接受一个新任务后,往往刚刚有了一个设计思想就去写程序,欲速则不达,越写越乱,最后重新翻工,在一次次的碰壁以后,完成一个基本正确或隐含错误的程序。磨刀不误砍柴工这句老话,用在此处很贴切。
■对糟糕的程序重新编写。
在程序第一遍写完之后,程序员的思路是清晰的,他晓得自己想怎样完成这样工作,在调试时,随着对程序的改动,他最初设计的潜在错误越来越多地暴露出来,这时他的注意力大部分集中在程序的正确性上,而忽略了程序的其他要求,程序往往越改越乱。
因此, 对糟糕的程序不要进行修补,要重新写!
■对程序的改动要保持清晰。
在开始设计的时候程序是比较清晰的,往往在修改的过程中使程序越来越背离初衷,难于理解,虽然功能达到了要求但程序的质量很差。因此在修改的过程中要从整体上考虑,千万不要只为达到功能需求而盲动。
■对程序要逐次求精,反复修改。
对程序的逐次求精是向着更加清晰,更加正确的方向。
例: 求100以内的素数。
以下的结论都是正确的
A. 如果一个数N>2不能被2,...,N-1 整除,则其为素数
B. 如果一个数N>2不能被2,...,SQR(N) 整除,则其为素数
C. 如果一个数N>2不能被2,...,SQR(N) 之间的素数整除,则其为素数
D. 任何大于2的素数均是奇数
根据上述结论,下面的三种算法都是正确的:
算法A.
N=100
I=3
DO WHILE I<=N
J=3
DO WHILE INT(I/J)*J<>I .AND. J<I
J=J+1
ENDDO
IF J=I
? I
ENDIF
I=I+2
ENDDO
算法B.
N=100
I=3
DO WHILE I<=N
J=3
DO WHILE INT(I/J)*J<>I .AND. J<SQR(I)
J=J+1
ENDDO
IF J=I
? I
ENDIF
I=I+2
ENDDO
算法C.
N=100
CURSOR=1
P(1)=3
I=5
DO WHILE I<=N
J=1
DO WHILE INT(I/P(J))*P(J)<>I .AND. P(J)<SQR(I) .AND. J<=POINTER
J=J+1
ENDDO
IF P(J)>=SQR(I)
CURSOR=CURSOR+1
P(CURSOR)=I
? I
ENDIF
I=I+2
ENDDO
如果要选择一种速度最快的算法,建议选用算法C,如果追求简洁性,则选择算法A比较合适.
■在没有经过详细测试之前,不要将你的程序提交给用户。
将测试不充分的程序提交到用户手中是很严重的错误,一方面会增加用户的不信任感,另一方面发现问题后,往往会陷入为改问题而改问题的境地,使程序不能在冷静的状态下进行有计划的修改。