跳转至

📔 Part IV : Statements 学习笔记

Chapter 14 组织直线型代码

组织直线型代码是一个相对简单的任务,但也会对代码有质量、正确性、可读性和可维护性带来影响。

1. 必须有明确顺序的语句

如果程序语句之间存在依赖关系,并且关系要求你把语句按照一定的顺序加以排序,则设法使得依赖关系更加明显。对于组织语句的简单原则如下:

  • 设法组织代码,使依赖关系变得明显。
  • 使子程序能突显依赖关系
  • 利用子程序参数明确显示依赖关系
  • 用注释对不清晰的依赖关系进行说明
  • 用断言或者错误处理代码检查依赖关系

2. 顺序无关的语句

  • 使代码易于自上而下地阅读
  • 把相关的语句组织在一起

3. 核对表:组织直线型代码

  • 代码使得语句之间的依赖关系变得明显吗?
  • 子程序的名字使得依赖关系变得明显吗?
  • 子程序的参数使得依赖关系变得明显吗?
  • 如果依赖关系不明显,你是否用注释来进行说明?
  • 用“内务管理变量” 来检查代码中关键位置的顺序依赖关系了吗?
  • 代码容易按照自上而下的顺序阅读吗?
  • 相关的语句被组织在一起吗?
  • 把相对独立的语句组放在各自的子程序里吗?

Chapter 15 使用条件语句

1. if语句

1.1 简单if-else语句

使用if语句的指导原则:

  • 首先写正常代码路径,再处理不常见情况
  • 确保对于等量的分支是正确的
  • 把正常情况的处理放在if后面而不要放在else后面
  • 把if子句后面跟随一个有意义的语句
  • 考虑else子句
  • 测试else子句的正确性
  • 检查if和else子句是不是弄反了

1.2 if-then-else语句

对于if-then-else串的指导原则:

  • 利用布尔函数调用简化复杂的检测
  • 把最常见的情况放在最前面
  • 确保所有的情况都考虑到了
  • 如果使用的语言支持,把if-then-else语句串替换成其他结构

2. case语句

对于case语句的一些指导原则:

  • 为case选择最有效的排序

    • 按字母顺序或按数字顺序排列各种情况
    • 正常的情况放在前面
    • 按执行频率排列case子句
  • 使用case语句的提示

    • 简化每种情况对应的操作
    • 不要为了使用case语句而可以制造一个变量
    • 把default子句只用于检查真正的默认情况
    • 利用default子句来检测错误
    • 在C++和Java里,避免代码执行越过一条case子句的末尾
    • 在C++里,在case末尾明确无误地标明需要穿越执行的程序流程

3. 核对表:使用条件语句

  • if-then语句

    • 代码的正常路径清晰吗?
    • if-then测试对等量分支的处理方式正确吗?
    • 使用else子句并加以说明吗?
    • else子句用得对吗?
    • 用对了if和else子句,即没把它们用反吗?
    • 需要执行的正常情况是位于if而不是else子句里吗?
  • if-then-else-if语句串

    • 把复杂的判断封装到布尔函数调用了吗?
    • 先判断最常见的情况了吗?
    • 判断包含所有的情况了吗?
    • if-then-else-if是最佳的表现吗?比case语句还要好吗?
  • case语句

    • case子句排序得有意义吗?
    • 每种情况的操作简单吗?必要的时候调用了其它子程序吗?
    • case语句检测的一个真实的变量,而不是一个只为了滥用case语句而刻意制造变量吗?
    • 默认子句用得合法吗?
    • 用默认子句来检测和报告意外之外的情况了吗?
    • 用C、C++或者Java里,每一个case的末尾都有一个break吗?

Chapter 16 控制循环

“循环” 是一个非正式的术语,用来指代任意一种迭代控制结构。---- 可以反复执行一段代码的结构。

1. 选择循环的种类

在大多数语言中,只能用到少数几种循环:

  • 计数循环执行的次数是一定的,可能是针对每位雇员执行一次;
  • 连续求值的循环预先并不知道将要执行多少次,它会在每次迭代时检查是否应该结束;
  • 无限循环一旦启动就会一直执行下去;
  • 迭代器循环对容器内的每个元素执行一次操作。

灵活度和检查位置决定了如何对用作控制结构的循环种类进行选择。

1.1 什么时候使用while循环

如果不知道循环多少次,就使用while循环。while循环的最主要的事项是决定循环的位置。

  • 检测位于循环的开始
  • 检测位于循环的结尾

1.2 什么时候用带退出的循环

带退出的循环:终止条件出现在循环中间而不是开始或者末尾的循环。

  • 正常的带退出循环

    • 一个带退出循环通常是由循环头、循环体(包括终止条件)和循环尾组成。
  • 非正常的带退出循环

1.3 何时使用for循环

  • 执行次数固定的循环,for就是一个很好的选择。
  • 主要用于不需要循环内部控制的简单操作。
  • 递增或递减

1.4 何时使用foreach循环

foreach循环或其等价物很适用于对数组或者其他容器的各项元素执行操作。

2. 循环控制

  • 进入循环

    • 只从一个位置进入循环
    • 把初始化代码紧放在循环前面
    • while(true)表示无线循环
    • 在适当的情况下多使用for循环
    • 在while循环更适用时,不要使用for循环
  • 处理好循环体

    • "{""}" 把循环中的语句括起来
    • 避免空循环
    • 把循环内务操作要么放在循环的开始,要么放在循环的末尾
    • 一个循环只做一件事
  • 退出循环

    • 设法确认循环能够终止
    • 使循环终止条件看起来很明显
    • 不要为了终止循环而胡乱改动for循环的下标
    • 避免出现依赖于循环下标最终取值的代码
    • 考虑使用安全计数器
  • 检查端点

    • 考虑在while循环中使用break语句而不是布尔标记
    • 小心那些有很多break散布其中的循环
    • 在循环开始处用continue进行判断
    • 如果语言支持,请使用带标号break结构
    • 使用break和continue时要小心谨慎
  • 使用循环变量

    • 用整数或者枚举类型表示数组和循环的边界
    • 在嵌套循环中使用有意义的变量名来提高其可读性
    • 用有意义的名字来避免循环下标串话
    • 把循环下标变量的作用域限制在本循环内
  • 循环应该有多长

    • 循环要尽可能地短,以便于能够一目了然
    • 把嵌套限制在3层以内
    • 把长循环的内容移到子程序里
    • 要让长循环格外清晰

3. 轻松创建循环 --- 由内而外

从一种情况开始,用字面量来编写代码。然后缩进它,在外面加上一个循环,然后用循环下标或计算表达式来替换字面量。如果需要,在它的外面再套上一个循环,然后再替换掉一些字面量。根据需要持续该过程,等做完之后,再加上所有需要的初始化。

从简单的情况开始并由内向外生成代码的过程,可以看做是由内而外的编码。

4. 循环和数组的关系

循环和数组之间都有密切的联系。在许多情况中,循环就是用来操纵数组的。而且循环计数器和数组下标之间一一对应。

5. 核对表:循环

  • 循环的选择和创建

    • 在合适的情况下用while循环取代for循环了吗?
    • 循环式由内到外创建的吗?
  • 进入循环

    • 是从循环头部进入循环的吗?
    • 初始化代码是直接位于循环前面的吗?
    • 循环是无限循环或者事件循环吗?它的结构是否清晰?
    • 避免使用像for i = 1 to 9999这样的代码吗?
    • 如果这是一个C++、C或Java中的for循环,那么把循环头留给循环控制代码了吗?
  • 循环的内部

    • 循环是否用了{ }或其他等价物来括上循环体,以防止因修改不当而出错吗?
    • 循环体内有内容吗?它是非空的吗?
    • 把内务处理集中地放在循环开始或者循环结束处了吗?
    • 循环短得足以一目了然吗?
    • 循环嵌套层次不多于3层吗?
    • 把长循环的内容提取成单独的子程序吗?
    • 如果循环很长,那么它非常清晰吗?
  • 循环下标

    • 如果这是一个for循环,那么其中的代码有没有随意修改循环下标值?
    • 是否把重要的循环下标值保存在另外的变量里,而不是在循环体外使用该循环下标?
    • 循环下标是序数类型(整数)或者枚举类型 —— 而不是枚举类型吗?
    • 循环下标的名字有意义吗?
    • 循环避免了下标的串话问题吗?
  • 退出循环

    • 循环在所有可能的条件下都能终止吗?
    • 如果你建立了某种安全计数器标准,循环使用安全计数器了吗?
    • 循环的退出条件清晰吗?
    • 如果使用了break或者continue,那么他们用对了吗?

Chapter 17 不常见的控制结构

1. 子程序中的多个返回

多数语言都提供了某种半途退出子程序的方法。程序可以通过return 和 exit 这类控制结构,在任何需要的时候退出子程序。

使用return语句的指导原则:

  • 如果能增强可读性,那么就使用return。
  • 用防卫子句(早返回或早退出)来简化复杂的错误处理。
  • 减少每个子程序中return的数量。

2. 递归

在递归里面,一个子程序自己负责解决某个问题的一小部分,它还把问题分解成很多的小块,然后调用自己来分别解决每个小块。

使用递归的技巧:

  • 确认递归能够停止
  • 使用安全计数器防止出现无穷递归
  • 把递归限制在一个子程序内
  • 留心栈空间
  • 不要用递归去计算阶乘或者斐波那契数列

3. goto

3.1 反对goto的论点

人们反对使用goto的普遍理由:没有使用goto的代码就是高质量的代码。

  • 含有goto的代码很难安排好格式
  • 使用goto会破坏编译器的优化特性
  • 含有goto的代码却极少是最快或者最小的。
  • 使用goto会违背代码应该自上而下运行的原则

3.2 支持goto的观点

goto的支持者们通常都会强调要在特定的场合下谨慎地使用goto,而不要不分青红皂白地使用。

  • 如果使用位置恰当,goto可以减少重复的代码。
  • goto在分配资源、使用资源后再释放资源的子程序里非常有用。
  • 在某些情况下,使用goto会让代码的运行速度更快,体积更小。
  • 编程水平高并不等于不使用goto。
  • 很多语言都已经包含togo。

3.3 错误处理和goto

以避免使用goto,所以可能的重写策略包括以下几种:

  • 用嵌套的if语句重写
  • 用一个状态变量重写代码
  • 用try-finally重写

3.4 goto使用原则总结

  • 在那些不直接支持结构化控制语句的语言里,用goto去模拟那些控制结构。
  • 如果语言中内置了等价的控制结构,则就不要用goto
  • 如果是为了提高代码效率而使用goto,请衡量此举带来的性能提升。
  • 除非要模拟结构化语句,否则尽量在每个子程序内只使用一个goto标号
  • 除非要模拟结构化语句,尽量让goto向前跳转而不要向后跳转。
  • 确认所有的goto标号都被用到了。
  • 确认goto不会产生某些执行不到的代码
  • 对某一个goto用法所展开的争论并不是事关全局。

4. 对不常见控制结构的看法

有些人认为下面列出的每一种控制结构都是很不错的想法:

  • 不加限制地使用goto
  • 能动态计算出goto的跳转目标并且执行跳转。
  • 用goto从一个子程序的中部跳转到另一个子程序的中部的能力
  • 根据行数或者标号来调用子程序,从而允许代码从子程序的中间的某个位置开始执行。
  • 具备让应用程序动态生产代码并且执行这些代码的能力。

5. 核对表:不常见的控制结构

  • return

    • 每一个子程序都仅在有必要的时候才使用return吗?
    • 使用return有助于增强可读性吗?
  • 递归

    • 递归子程序中包含了停止递归的代码吗?
    • 子程序用安全计数器来确保该子程序能停下来吗?
    • 递归只位于一个子程序里面吗?
    • 子程序的递归深度处于程序栈容量可以满足的限度内吗?
    • 递归是实现子程序的最佳方法吗?它要好于简单的迭代吗?
  • goto

    • 是否只有在万不得已的时候才使用goto?如果用了goto,是否仅仅是出于增强可读性和可维护性呢?
    • 如果是出于效率因素而使用的goto,那么对这种效率上的提升作出衡量并且加以说明了吗?
    • 一个子程序里最多只用了一个goto标号吗?
    • 所有的goto都向前跳转,而不是向后跳转吗?
    • 所有的goto标号都用到了吗?

Chapter 18 表驱动方法

表驱动法是一种编程模式(scheme) --- 从表里面查找信息而不使用逻辑语句(if和case)。

1. 表驱动方法使用总则

在使用表驱动法时,必须要解决两个问题:

  • 必须要回答怎样从表中查询条目的问题
  • 应该在表里面存些什么。

2. 直接访问表

和所有的查询表一样,直接访问表代替了更为复杂的逻辑控制结构。之所以说它们是“直接访问”的,是因为你无须绕很多复杂的圈子就能够在表里面找到你想要的信息。

可以讲数据作为键值直接访问表。对于构造一些键值的方法:

  • 复制信息从而能够直接使用键值。
  • 转换键值以使其能够直接使用
  • 把键值转换提取成独立的子程序

3. 索引表访问

有时候,只用一个简单的数学运算还无法把类似age的数据转换成表键值,所以此时需要索引访问的方式来解决。

使用索引时,先用一个基本类型的数据从一张索引表中查出一个键值,然后再用这一键值查出感兴趣的主数据。

索引表不是直接访问,而是经居间的索引去访问。

索引访问技术有的主要优点:

  • 如果主查询表中的每一条记录都很大,那么创建一个浪费很多空间的索引数组所用的空间,就要比创建一个浪费了很多空间的主查询表所用的空间小得多。
  • 用了索引以后没有节省内存空间,操作位于索引中的记录有时要比位于主表中的记录更方便廉价。
  • 表查询技术在可维护性上强。

4. 阶梯访问表

不像索引结构式的那样直接,但要比索引访问方法更节省空间。

阶梯结构的基本想法:表中的记录对于不同的数据访问有效,而不是对不同的数据点有效。

阶梯方法通过确定每项命中的阶梯层次确定其归类,它命中的 “台阶” 确定其类属。

优点:

  • 阶梯式很适合于处理那些无规则的数据。

对于使用阶梯技术时需要注意的一些细节:

  • 留心端点
  • 考虑用二分查找取代顺序查找
  • 考虑用索引访问来取代阶梯技术
  • 把阶段表查询操作提取成单独的子程序

5. 核对表:表驱动法

  • 你考虑过把表驱动法作为复杂逻辑的替换方案了吗?
  • 你考虑过把表驱动法作为复杂继承结构的替换方案了吗?
  • 你考虑过把表数据存储在外部并在运行期间读入,以便在不修改代码的情况下就可以改变这些数据吗?
  • 如果无法用一种简单的数组索引去访问表,那么你把计算访问键值的功能提取成单独的子程序,而不是在代码中重复地计算键值吗?

Chapter 19 一般控制问题

1. 布尔表达式

除了最简单的、要求语句按顺序执行的控制结构之外,所有的控制结构都依赖于布尔表达式的求值(evaluation)。

  • truefalse 做布尔判断

    • 在布尔表达式中应该使用标识符 truefalse,而不要用01等数值。
    • 隐式地比较布尔值与 truefalse
  • 简化复杂的表达式

    • 拆分复杂的判断并引入新的布尔变量
    • 把复杂的表达式做成布尔函数

      • 如果某项判断需要重复做,或者会搅乱对程序主要流程的理解,那么可以把该判断的代码提取成一个函数,然后判断该函数的返回值。
    • 用决策表代替复杂的条件

  • 编写肯定形式的布尔表达式

    • 在if语句中,把判断条件从否定形式转换为肯定形式,并且互换ifelse子句中的代码。
    • 用狄摩根定理(Demorgan's Theorems)简化否定的布尔判断
      • 一个表达式与另一个含义相同但却以双重否定形式表达的表达式之间的逻辑关系。
  • 用括号使用布尔表达式更清晰

    • 用一种简单的计数技巧来使括号对称。
    • 把布尔表达式全括在括号里面
  • 理解布尔表达式是如何求值的

    • 一些语言的编译器采用 “短路” 或者 “惰性” 求值。
    • 不同的语言所用的求值方法是不同的。
  • 按照数轴的顺序编写数值表达式

  • 与0比较的指导原则

    • 隐式地比较逻辑变量
    • 把数和0相比较
    • 把指针与NULL相比较
  • 布尔表达式的常见问题

    • 在C家族语言中,应该把常量放在比较的左端

      • 可以让编译器来捕获会存在的异常错误。
    • 在C++中,可以考虑创建预处理宏来替换 &&||==

    • 在Java中,应理解 a==ba.quals(b) 之间的差异。

2. 复合语句(块)

“复合语句” 和 “复合块” 指的是一组语句,该组语句被视为一条单一的语句,用于控制程序流。

使用复合语句的一些指导原则:

  • 把括号对一起写出
  • 用括号来把条件表达清楚

3. 空语句

在C++中,可以写空语句,即一条仅含有分号的语句

C++中处理空语句的指导原则

  • 小心使用空语句
  • 为空语句创建一个DoNothing() 宏预处理宏或者内联函数
  • 考虑如果换用一个非空的循环体,是否会让代码更清晰。

4. 存在陷阱的深层嵌套

要避免使用深层嵌套的理由:深层嵌套与软件构造设计中的首要技术使命 ---- 管理复杂度 ---- 存在相悖。

避免深层嵌套的方法:

  • 通过重复检测条件中的某一部分来简化嵌套的if语句
  • 用break来简化嵌套if
  • 把嵌套if转换成一组if-then-else语句
  • 把嵌套if转换成case语句
  • 把深层嵌套的代码抽取出来放进单独的子程序
  • 使用一种更面向对象的方法
  • 重新设计深层嵌套的代码

对减少嵌套层次的技术的总结:

  • 重复判断某一部分条件
  • 转换成if-then-else
  • 转换成case语句
  • 把深层嵌套的代码提取成单独的子程序。
  • 使用对象和多态派分
  • 用状态变量来重写代码
  • 用防卫子句来退出子程序,从而使得代码的主要路径更为清晰。
  • 使用异常
  • 完全重新设计深层嵌套的代码。

5. 编程基础:结构化编程

结构化编程的核心思想:一个应用程序应该只采用一些单入单出的控制结构(也称为单一入口单一出口的控制结构)。

结构化编程的三个组成部分:

  • 顺序

    • 一组按照先后顺序执行的语句。
    • 典型的顺序型语句:赋值和调用子程序。
  • 选择

    • 是一种有选择的执行语句的控制结构。
    • 典型例子:
      • if-then-else语句
      • switch-case语句
  • 迭代

    • 一种使一组语句多次执行的控制结构。迭代常常也称为 “循环”。
    • 典型例子:
      • for循环。

6. 控制结构与复杂度

“程序复杂度” 的一个衡量标准是,为了理解应用程序,必须在同一时间记住的智力实体的数量。

单从直觉来说,程序的复杂度看来在很大程度上决定了理解程序需要花费的精力。

  • 降低复杂度的一般原则
    • 可以采用两种方法
      • 通过一些脑力练习来提高自身的脑力游戏水平。
      • 可以降低应用程序的复杂度

把复杂度降低到最低水平才是编写高质量代码的关键。

7. 核对表:控制结构相关事宜

  • 表达式中用的是truefalse,而不是01吗?
  • 布尔值和true以及false做比较是隐式进行的吗?
  • 对数值做比较是显式进行的吗?
  • 有没有通过增加新的布尔变量、使用布尔函数和决策表来简化表达式?
  • 布尔表达式是用肯定形式表达的吗?
  • 括号配对吗?
  • 在需要用括号来明确的地方都使用了括号吗?
  • 判断是按照数轴顺序编写的吗?
  • 如果适当的话,Java中的判断的是a.quals(b)方式,而没有用a==b方式吗?
  • 空语句表述得明显吗?
  • 用重新判断部分条件、转换成if else或者case语句、把嵌套代码提取成单独的子程序、换用一种更面向对象的设计或者其他的改进方法来简化嵌套语句了吗?
  • 如果一个子程序的决策点超过10个,那么能提出不重新设计的理由吗?