Featured image of post 读《代码大全(第二版)》笔记(四)

读《代码大全(第二版)》笔记(四)

本周读了第四部分——语句

这一部分包括的章节比较多,首先分别用 三 个章节讲述了三种常见的语句结构,随后用一章的内容谈论了一下不常见的控制结构。在使用控制结构方面,为了降低复杂度,本部分专门用一章讲述了一种方法,最后用一章内容描述了一下常见的控制问题,本部分总共包括 六 章内容。

第十四章——组织直线型代码

本章篇幅比较短,主要讲述了一些如何让用户从外部调用对象方法的时候能够清楚的遵循一定的顺序。换句话说,就是让别人调用你的方法的时候能够简单明了的知道这些方法的调用顺序。书中给出了一些指导原则。

(Page.348)设法组织代码,使依赖关系变得非常明显

我自己的理解是,在封装类的时候,从内部尽量使方法之间的依赖降到最低。在方法功能的实现上尽可能的做到功能单一,我觉得可以比较好的解决这个问题。应该尽可能的满足 KISS 原则吧。

(Page.348)使子程序名能凸显依赖关系

这条原则的意思很明显,就是对所定义的方法要取个清晰的名字。

(Page.349)利用子程序参数明确显示依赖关系

这个原则也不用解释过多,书中提到了一种情况,就是把几个方法的参数定义成同一个类型,以此暗示这些方法在调用参数的时候需要留心它们之间的调用关系。我自己倒是觉得,或许还可以有一种情况,那就是规定调用的 A 方法能够产生调用 B 方法所需要的参数类型,这样 A、B 方法的调用也可以从外部有一个依赖关系。

(Page.350)用注释对不清晰的依赖关系进行说明

这种方法我认为属于下策,我觉得,任何通过注释进行说明的方式都是下策,我觉得好的代码应该尽量不依赖于注释,也许我的想法有些偏激,因为我觉得代码本身就是给人读的,如果通过代码仍让人琢磨不清,可能这段代码的设计偏离了 KISS 原则。当然,注释能够更好的辅助我们快速了解代码,我的意思并不是说写了注释的代码就是烂代码,不要走极端。

(Page.350)用断言或者错误处理代码来检查依赖关系

对于重要的代码,尽量在方法内部加上断言。其实,可以看出来,这属于防御式设计了,为了避免系统的崩溃,所采取的必要措施。

本章后面简单说了一下,对于顺序无关的语句应该如何书写。提到了两个大的要点,一个是

(Page.351)使代码易于自上而下地阅读。

另一个是

(Page.352)把相关地语句组织在一起。

这两个观点都很明显,为的就是使代码阅读起来更有条理。

第十五章——使用条件语句

本章主要是对条件语句的书写进行了讲解。条件语句分为两大类,一类是 if 语句,另一类为 case 语句。

对于 if 语句,书中给出了一些指导原则(Page.355~Page.358)。

  • 首先写正常代码路径;再处理不常见情况。
  • 确保对于等量的分支是正确的。请不要用 > 代替 >= 或用 «/code> 代替 <= ,这类似于在访问数组或者计算循环下标的时候犯下 off-by-one (偏差一)错误。
  • 把正常情况的处理放在 if 后面而不要放在 else 后面
  • if 子句后面跟随一个有意义的语句
  • 考虑 else 子句。如果你认为自己只需要一个简单的 if 语句,那么请考虑你是否真的不需要一个 if-then-else 语句。
  • 测试 else 子句的正确性
  • 检查 ifelse 子句是不是弄反了

上面只是简单罗列了书中的一些条款,其实具体的细节,需要不断的在书写代码的时候进行调整。

在这里,我想说两句自己偶然想到的一些认识。平时很多大牛在介绍经验的时候会经常说,“代码要多写,没有其他捷径”。针对这句话的理解,我想说两句自己的想法,之前听了这些话,听起来觉得挺有道理,但是却不知道为什么,总是用熟能生巧来简单的搪塞自己。书看到这部分的时候,我突然意识到,写代码这项运动,其实不是简单的提高打字速度,也不单单是熟练使用语言中的关键字。其实,更多的涉及到的部分是习惯和思维方式的培养问题,通过不断在代码上的调整,来调整我们的思维方式,之后通过不断的重复,让这种调整后的行为逐渐的成为我们的习惯,以此来逐渐的规范我们的代码编写方式。久而久之,即便在你无意识的情况下也可以反射性的写出高质量的代码。这个时候我想就达到了代码要多写的目的。

恩,经过这样的简单分析,我就基本上理解了大牛们说的代码要多写的真正含义。好吧吧,也许有朋友看到这里觉得我有点死较真儿,或者说这么明显的道理都看不出来,如果屏幕前的您早就想到了,对着屏幕呵呵呵就行了,见谅!:)。坦白说,之前确实没有想太多,或者说,总感觉多写代码为的只是增加对某种语言的关键字使用的熟练程度,真的没有意识到其实最终为的是改变你的思维方式和行为习惯。

好吧,说了两句突发奇想的感悟,下面还回到正题上来。接下来说的是 case 语句。对于 case 语句的有效排列顺序而言,书中给出了三种建议。

  • 按字母排序或按数字排序排列各种情况
  • 把正常的情况放在前面
  • 按执行频率排列 case 子句

上面提到的三种排列建议都比较好理解。接下来作者给出了几个使用 case 语句的小提示。

(Page.361)简化每种情况对应的操作

其实,就是尽可能让每个 case 里的功能单一。

(Page.361)不要为了使用 case 语句而刻意制造一个变量

这句话也很好理解,不要为了 casecase ,凡事都有个度。

(Page.363)把 default 子句只用于检查真正的默认情况

这句比较好理解,不要滥用 default

第十六章——控制循环

本章主要讲循环语句。对于 for 循环的使用,书中做了说明。

(Page.372)如果你需要一个执行次数固定的循环,那么 for 循环就是一个很好的选择。

除此之外,书中对于何时用 while 循环代替 for 循环也做了说明。

(Page.372) for 循环的关键之处在于,你在循环头处把它写好后就可以忘掉它了,无需在循环的内部做任何事情去控制它。如果存在一个必须使执行从循环中跳出的条件,那么就应该改用 while 循环。

其实,对于 while 循环,说实话,我在代码中很少使用,一般情况下用 for 或者使用foreach循环就够用了。在 Golang 语言中也确实去掉了 while 循环,我想这也说明了 while 语句的使用确实可以被替代。

对于循环控制,书中给出了一些建议。

(Page.374)用 while(true) 表示无限循环

对于无限循环的使用,就用上面的建议就行了。不过,如果不涉及网络或动画,一般也用不到无限循环。

(Page.374)在适当的情况下多使用 for 循环

其实从 Golang 语法中去掉 while 关键字已经很说明问题了。

对于处理循环体,书中也给出了几条建议。

(Page.375)用 { 和 } 把循环中的语句括起来。任何时候都要在代码中使用括号。

这一习惯可以让别人更清晰的阅读代码。

(Page.375)避免空循环

我倒是没有这种习惯,一来觉得影响美观,二来会让代码看起来不是很清晰。

(Page.376)把循环内务操作要么放在循环的开始,要么放在循环的末尾。

这条建议目的觉得就是为了让书写代码更有规律可循,避免胡乱摆放代码。

(Page.376)一个循环只做一件事

这条建议很直观,遵守 KISS 原则。

对于循环的退出,书中也给出了一些建议。

(Page.377)不要为了终止循环而胡乱改动 for 循环的下标

这个错误我好像从来没干过。还有下一条建议从来没尝试过,从来没想过这么用,看了书中的例子才知道,原来还可以酱紫犯错误,也算是开眼了。

(Page.377)避免出现依赖于循环下标最终取值的代码

以上两点除了让代码更紧凑以外,还主要是为了尽可能的不要让 bug 蔓延。书中还提到一个方法,老实说我之前没想到过,长姿势了。

(Page.378)考虑使用安全计数器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//C++示例:本应使用安全计数器的代码
do {
    node = node->Next;
    ...
} while (node->Next != NULL );

// 下面是使用了安全计数器的同一代码段:
//C++示例:使用了安全计数器的代码
safetyCounter = 0;
do {
    node = node->Next;
    ...
    safetyCounter++;    //安全计数器代码
    if (safetyCounter >= SAFETY_LIMIT) {        //安全计数器代码段
        Assert(false, "Internal Error: Safety-Counter Violation.");
    }
    ...
} while (nod->Next != NULL);

关于安全计数器,有利就有弊。书中说出了其中的利弊,

(Page.379)每次都在代码里使用一个安全计数器,也会增加复杂度,并且可能引发其他的错误。…然而,如果把安全计数器作为整个项目的一种标准应用于关键的循环,那么含有安全计数器的代码不会比其他的代码更容易出错。

(Page.383)把循环下标变量的作用域限制在本循环内

关于这一点,书中后面也说了,不同的 C++ 编译器会对此语法做出不同的反映。所以如果想用 C++ 做跨平台开发,个人建议还是把循环变量放到循环体外进行声明。例如,

1
2
3
4
5
//C++示例
int num;
for (num = 0; num < MAX; num++){
    ...
}

关于循环代码的长度,书中给出了一些建议 (Page.385)。

  • 循环要尽可能的短,以便能够一目了然。如果你开始接受编写简单代码这一原则,那就很少会写出超过 15 或者 20 行的循环。
  • 把嵌套限制在 3 层以内
  • 把长循环的内容移到子程序里
  • 要让长循环格外清晰

都是一些书写规则,很醒目,就不多做解释了。

第十七章——不常见的控制结构

本章的主要内容有两方面,一方面是递归。另一方面是容易引起纠纷的 goto 语句。

关于递归的使用,书中有一段话做了概要性的说明。

(Page.394)对于大多数问题,它所带来的解将会是及其复杂的——在那些情况下,使用简单的迭代通常会比较容易理解。因此要有选择地使用递归。

使用递归技巧方面,有几点还是需要注意的。

(Page.396)把递归限制在一个子程序内。循环递归( A 调用 B,B 调用 C,C 调用 A )非常危险,因为它很难检查。

对于这一点,我想我还真没遇到过,不过也可能是我遇到了没查出来…

(Page.397)留心栈空间

对于这一点书中做了解释,

(Page.397)应注意观察递归函数中局部变量的分配情况,特别要刘毅那些内存消耗大的对象。换句话说,要用new在堆(heap)上创建对象,而不要让编译器在栈上自动创建对象。

下面要说到 goto 语句,对于 goto 我不想多说,免得引火烧身。我发表两点看法,第一点是在合适的地方使用 goto,所谓合适的地方是,明显能降低复杂度而又不让代码的可读性变的混乱的情况下。第二点是在 Golang中依然保留了goto  的位置。

第十八章——表驱动法

本章的第一句话概括的很好,

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

其实,看完整个章节,我的感受只有一条,就是用空间换时间,降低控制的复杂度。

关于从表里面查找信息的方法。书中描述了三种方法:

  • 直接访问(Direct access)
  • 索引访问(Indexed access)
  • 阶梯访问(Stair-step access)

关于这三种方法的使用方式,我觉得结合书中的例子看更容易理解。当然了,对于访问表数据,核心问题无非就是查询。具体的方法就不说了,因为书中的方法都是针对了具体的例子而言。只是干巴巴的说,看起来会有很多逻辑漏洞。

第十九章——一般控制问题

本章主要是对前面几章所说的控制方式中常见的问题进行了描述。

首先说的是布尔表达式。

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

随后,书中主要在讲使用布尔表达式时候的注意事项。这里就不多说了,都是一些常见的问题,点很多。但主导思想还是 KISS 原则。

接下来对复合语句或语句块做了简单的描述,并建议尽量用括号把条件表达清楚等等。

对于如何减少深层嵌套,书中做了技术总结。

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

关于结构化编程,讲到了结构化编程的核心思想,

(Page.454)…那就是一个应用程序应该只采用一些单入单出的控制结构(也称为单一入口、单一出口的控制结构)。

结构化编程由三个部分组成(Page.454~Page.456):

  • 顺序(Sequence)
  • 选择(Selection)
  • 迭代(Iteration)

结构化编程的中心论点,书中也做了说明。

(Page.456)…任何一种控制流都可以由顺序、选择和迭代这三种结构生成。

关于如何度量复杂度,书中给出了一种计算方法。

  1. 从 1 开始,一直往下通过程序
  2. 一旦遇到以下关键字,或者其同类的词,就加 1: ifwhilerepeatforandor
  3. case 语句中的每一种情况都加1

下面举书中的例子(Page.458):

1
2
if ( ( (status = Success) and done ) or
( not done and ( numLines >= maxLines ) ) ) then ...

在这段代码中,从 1 算起,遇到 if 得 2,and  得 3,or  得 4,and 得 5。加起来这段代码里总共包含了 5 个决策点。

关于决策结果的评判,书中也给出了评判标准(Page.458):

数值范围结果描述
0 —— 5子程序可能还不错
6 —— 10得想办法简化子程序了
10+把子程序的某一部分拆成另一个子程序并调用它

整体上来说,这部分的主要内容就是控制结构,这几章都紧扣主题,而且我发现所有实施的办法都是为了控制复杂度,到目前为止,和之前的猜测还一直比较吻合。总体策略上,我发现主线就是遵守KISS原则。