这部分主要讲代码的控制结构相关的内容。
首先是最简单的代码控制流:即按先后顺序放置语句和语句块。一些顺序型的语句是必须有明确顺序的,因为它们之间存在依赖性。我们往往需要突出这些依赖性,需要遵循一些简单的原则:
- 设法组织代码,使依赖关系变得非常明显;
- 使子程序名能凸显依赖关系;
- 利用子程序参数明确显示依赖关系;
- 用注释对不清晰的依赖关系进行说明;
- 用断言或者错误处理代码来检查依赖关系;
此外,一些顺序组织的语句可能是顺序无关的。这时应该遵循就近原则,把其中相关的操作放在一起,从而让程序易于自上而下的阅读,而不用让读者的目光跳来跳去。
条件语句。条件语句是用来控制和决定其他语句是否执行的。对于最普遍和通用的 if
语句的一些指导原则:
- 首先写正常代码路径,再处理不常见情况;
- 确保对于等量的分支是正确的;
- 把正常情况的处理放在 if 后面而不要放在 else 后面;
- 利用布尔变量或者布尔函数调用来简化复杂的检测;
- 把最常用的情况放在最前面;
- 确保所有的情况都考虑到了;
- 如果你的语言支持,请把
if-then-else
语句替换成其他结构(例如switch-case
);
对于 switch
条件语句。要考虑为 case 选择最有效的排列顺序:按字母顺序或者数字顺序排列各种情况;把正常的情况放在前面;按执行频率排列 case 子句。应该简化每个 case 对应的操作,必要的时候提取子程序。将 default 子句用于真正的默认情况或者用来检测错误。
控制循环。用来指代任意一种迭代控制结构。常见的有:for, while 和 do-while。 主要有这样几种循环:计数循环;连续求值的循环;无限循环;迭代器循环。它们可能具有不同的灵活度和判断循环的位置。如果你预先并不知道循环要迭代多少次,那么就使用 while 循环。可以通过调用 goto 语句或者在 while 中使用 break 语句创建带退出的循环。如果你需要一个执行次数固定的循环,那么 for 循环就是一个很好的选择。它的关键之处在于,你在循环头部处理循环,无须在循环的内部做任何事情去控制它。foreach 循环或其等价物很适合用于对数组或者其他容器的各项元素执行操作。尽量做到可以把循环体当作一个黑盒来看待,从而使我们不需要去关注循环实现的细节。在这方面,更函数式的迭代器是比较好的选择。对于那些在循环中需要循环变量的一些指导意见:
- 用整数或者枚举类型表示数组和循环的边界;
- 在嵌套循环中使用有意义的变量名来提高其可读性;
- 用有意义的名字来避免循环下标串话;
- 把循环下标变量的作用域限制在本循环内;
循环要尽可能地短,以便能够一目了然;要把嵌套限制在 3 层以内;把长循环的内容移到子程序里。
还有一些不常见的控制结构。例如可以提前从子程序中返回的 return 语句,递归,goto 等。对于 return 语句:如果能增强可读性,那么就使用 return;用防卫子句(早返回或早退出)来简化复杂的错误处理;减少每个子程序中 return 的数量。要谨慎使用递归,对于某一小范围的问题,使用递归会带来简单,优雅的解。在稍大一些范围里,使用递归会带来简单,优雅但是难懂的解。对于大多数问题,它所带来的解将会是极其复杂的———在那些情况下,使用简单的迭代通常会比较容易理解。因此要有选择地使用递归。使用递归时要注意:
- 确认递归能够停止;
- 使用安全计数器防止出现无穷递归;
- 把递归限制在一个子程序内;
- 要留心栈空间,防止栈溢出;
- 不要用递归去计算阶乘或者斐波那契数列;
书中没有提及的是可以使用尾递归优化处理递归占用大量栈空间甚至导致栈溢出的问题。不到万不得已,不要使用 goto。很大程度上,软件开发这一领域是在限制程序员对代码的使用中得到发展的。例如考虑到 goto 带来的巨大灵活性的同时也带来了复杂度提升和维护性降低的弊端。所以应该尽量使用常见的控制结构。
表驱动法是一种可以作为常规的选择语句的良好替代的编程模式。凡是能通过逻辑语句来选择的事物,都可以通过查表来选择。随着逻辑链的越来越复杂,表驱动法就愈能显示出其灵活,高效,易于修改的优势。在使用表驱动法的时候,必须要解决两个问题:怎样查询条目的问题,表存储内容的问题。
最后,还有一些一般的控制语句问题。对于布尔表达式的一些指导原则:
- 要尽可能的使用标识符
true
和false
而不是 0 ,1 等数值; - 拆分复杂的判断并引入新的布尔变量,或者写成布尔函数;
- 必要的时候,用决策表代替复杂的条件;
- 用括号使布尔表达式更清晰;
- 注意特定语言中布尔表达式的“短路”或者“惰性求值”的特性;
- 按照数轴的顺序编写数值表达式;
对于复合语句,应该用括号把 if
语句的主体部分括起来,无论块内的代码行数是 1 还是 20。对于深层嵌套,很少有人能够理解超过 3 层的 if 嵌套。所以有如下一些避免深层嵌套的方法:
通过重复检测条件中的某一部分来简化嵌套的 if 语句;
例如:
1//糟糕的生成嵌套代码2 if ( inputStatus == InputStatus_Success ) {3 // lots of code4 //...5 if ( printerRoutine != NULL) {6 // lots of code7 //...8 if ( SetupPage() ) {9 // lots of code10 //...11 if ( AllocMem( &printData ) ) {12 // lots of code13 //...14 }15 }16 }17 }1819 // 利用重复检测的非嵌套代码20 if ( inputStatus == InputStatus_Success ) {21 // lots of code22 //...23 if ( printerRoutine != NULL ) {24 // lots of code25 //...26 }27 }2829 if ( (inputStatus == InputStatus_Success) &&30 ( printerRoutine != NULL ) && SetupPage() ) {31 // lots of code32 //...33 if ( AllcoMem( &printData ) ) {34 // lots of code35 //...36 }37 }
这个例子表明你不能无偿地减少嵌套层次,作为减少嵌套层次的代价,你必须要容忍使用一个更复杂的判断。
- 用 break 块来简化嵌套 if;
- 把嵌套 if 转换成一组 if-then-else 语句;
- 把嵌套 if 转换成 case 语句;
- 把深层嵌套的代码抽取出来放进单独的子程序;
- 使用一种更面向对象的方法;
复杂的代码表明你还没有充分地理解你的程序,所以无法简化它。深层嵌套是一个警告,它说明你要么应该拆分出一个子程序,要么应该重新设计那部分复杂代码。
关于结构化编程。结构化编程的核心思想很简单,那就是一个应用程序应该只采用一些单入单出的控制结构(也称为单一入口,单一出口的控制结构)。它适用于具体编码层。一个结构化的程序将按照一种有序的且有规则的方式执行,不会做不可预知的随便跳转。结构化编程有三个组成部分:顺序,选择,迭代。它的中心论点是,任何一种控制流都可以由顺序,选择和迭代这三种结构生成。对于三种标准的结构化编程结构之外的任何控制结构的使用(break, continue, return, throw-catch 等)我们都应该谨慎一些。控制结构会对程序的整体复杂度造成非常大的影响,如果用得不好就会增加复杂度。“程序复杂度”的一个衡量标准是,为了理解应用程序,你必须在同一时间记住的智力实体的数量。就直觉而言,程序的复杂度很大程度上决定了理解程序所需要花费的精力。我们可以通过提高自身的理解水平(一般提升不会太大)或者降低应用程序的复杂度,以及为了理解它所需要的专心程度。Tom McCabe 关于度量复杂度的方法:
- 从 1 开始,一直往下通过程序;
- 一旦遇到这些关键字,或者其同类型的关键字,就加 1: if, while, repeat, for, and, or;
- 给 case 语句中的每一种情况都加 1;
理想的情况下复杂度应该控制在 5 以内,如果在 6-10 的范围就要想办法简化子程序了,超过 10 一般就要考虑拆分出子程序了。还有其他一些度量复杂度的方法,包括:所用的数据量,控制结构中的嵌套层数,代码行数,对同一变量的先后引用之间的代码行数(跨度),变量生存的代码行数(生存期),以及输入和输出的量,等等。将复杂度降低到最低水平是编写高质量代码的关键。
这一部分,给我比较大启发的地方有:关于条件语句的使用指导,例如应该按照数轴上的顺序来编写数值表达式和对于减少嵌套层次的方法;关于结构化编程,及其与程序复杂度之间关系的讨论。谨慎的选择代码的结构才能更好的控制代码的复杂度,从而写出更高质量的代码。