编程精粹-Microsoft编写优质无错代码的秘诀(Writing Clean Code)

Table of Contents

1. 假想的编译程序

  • 不要期待好运气会碰到错误,应该去自己主动发现错误,排除运气对程序测试的影响,主动地抓住错误每个机会
  • 这章作者假想了一个非常智能的编译器,可以通过修改一些C语言规则,进行函数原形的检查和提供警告消息主动的发现错误,事实上能够利用现成的C编译器实现这些功能
  • 勤用Lint程序进行检查
  • 做单元测试(不要因为修改很平常而放弃单元测试)

2. 自己设计并使用断言

  • 对于每个程序最好都建立一个DEBUG版本,即要维护程序的交付版本,又要维护程序的调试版本
  • 作者对用断言Assert进行了一些探讨并进行封装,建议利用断言对函数参数进行确认
  • 从程序中删除无定义的特性,或者在程序中使用断言来检查出无定义特性的非法使用(对于无定义的特性,应该用断言补救)
  • 详细说明不清楚的断言,特别对一个难以理解的断言
  • 对自己的假定(或认为是不可能的情况),利用断言检验其正确性
  • 进行防错型程序设计时,不要隐瞒错误,必须及时地报告
  • 利用不同的算法对同一个东西进行检验,即使会降低程序运行速度

3. 为子系统设防

  • 作者认为在调用子系统时,是使用断言的最好时机,利用断言进行参数检验或是检验假设和不可能的情况
  • 消除随机特性,使错误可再现(作者利用内存分配的一个例子,在无用的内存中填入特定的字符如0xa3,来消除随机的特性,防止对于无用内存的引用,可以很容易的发现问题
  • 冲掉无用的信息,以免错误使用,说到底,就是为了将错误早点暴露出来
  • 保存调试信息,方便进行更强的错误检查
  • 建立详尽的子系统检查并且经常地进行这些检查
  • 对于测试代码也需要深思熟虑
  • 对于一致性,正确形的检查,应该做到透明
  • 在交付版本中,应该将内部影响性能的内部调试检查代码全部清除

4. 对程序进行逐条跟踪

  • 不要等到出现错误在逐语句调试程序,在编写一个子函数就应该逐语句调试,验证正确性
  • 对每一条代码路径跟踪调试
  • 密切关注数据流,因为通过观察数据流能够真正理解程序是否运行正确
  • 源码调试程序可能会隐瞒执行的细节,对关键部分代码(设计代码优化)需要汇编指令的跟踪

5. 糖果机界面

这里作者所谈及的就是怎么编写通过良好的函数界面来编写无错代码

  • 使用户不容易忽视错误情况,对于返回值中最好能够获得错误代码而不是隐藏
  • 不遗余力地寻找消除函数界面的缺陷
  • 作者通过对于realloc这个失败的函数的分析,建议不要编写将多种功能于一身的函数,对于函数参数必须进行进行更强的确认,编写功能单一的函数
  • 明确定义函数的参数
  • 编写函数是其在给定的有效输入下不会失败并且函数调用要简单明了
  • 编写注解突出可能的异常情况,最好的方法就是编写一个模范调用的例程

6. 风险事业

这里作者所谈论的就是对于数据移植,数据范围,和一些含有风险性的语句

  • 使用严格定义的数据类型,尽量使用可移植的数据类型,考虑尽可能的能够跨平台
  • 经常反问:”这个变量或是表达式会上溢或下溢”
  • 精确的实现设计,近似实现设计就可能出错误
  • 一次任务就应该一次完成,一个函数就应该完成特定的任务
  • 避免无关紧要的if,避免嵌套使用?:运算符(可读性可执行效率),每种特殊情况只能处理一次
  • 避免使用有风险的语言惯用语
  • 不能毫无必要地将不同类型的操作符混合使用,如果不清楚优先级,用括号将它们隔离开来
  • 避免调用返回错误的函数

7. 编码中的假象

  • 只引用属于你自己的存储空间
  • 只有系统才能拥有空闲的存储区,程序员不能拥有
  • 指向输出的指针不是指向工作空间缓冲区的指针
  • 不要去静态(或全局)两存储区传递数据,很容易出现问题,私有数据要自己管
  • 不要写寄生函数,即依赖于其他函数的实现
  • 不要滥用程序设计语言,不要依赖于程序设计语言的特定技巧细节
  • 紧凑的C代码并不能保证得到高效的机器代码(这点太对了,不然的话,优化编译器就不需要了)
  • 为一般水平的程序员编写代码(你认为x<<=1;和x/=2哪个效率高,事实上采用优化编译器两者一样高)

8. 剩下来的就是态度问题

  • 错误不会消失,错误只会因为环境设置的改变而掩盖,而不会无故消失。错误小时有三个原因:错误报告不对,错误被别的程序员修改,错误没有表现出来
  • 马上修改错误,不要推迟到最后
  • 修改错误要治本,不要治表
  • 除非关系产品成败,否则不要整理代码
  • 不要事先没有战略意义的特征(这个特征很好吗?但是它是为服务实现的还是为挑战你的技术实现的,如果是为挑战技术而实现,请删)
  • 不设自由特征(自由特征需要更多的编码,更多的测试,更多的人来编写文档)
  • 不要允许没有必要的灵活性
  • 在找到正确的解法之前,不要一味地“试”,要花时间寻求正确的。(试一试是一个忌讳词。试一试很可能会为了求解答儿饥不择食,最后的解不规范可能还有副作用,通过查取文档,多读少试来解决问题才是最好的办法)
  • 编写和测试小块代码,即使测试代码会影响进度,也要坚持测试代码
  • 测试代码的责任不在测试员身上,而是在程序员自己的责任
  • 不要责怪测试员发现了你的错误,你应该感谢测试员,因为测试员帮助你避免交付错误
  • 建立自己的优先级列表并坚持之。(正确性,可测试性,全局效率,可维护性,一致性,大小,局部效率,个人表达方式,个人方便性)

9. 编码检查表

一般问题

  • 你是否为程序建立了DEBUG版本
  • 你是否将发现的错误及时改正了
  • 你是否坚持彻底调试编码,即使耽误了进度也在所不辞
  • 你是否依靠测试组为你测试编码
  • 你是否知道编码的优先顺序
  • 你的变异程序是否有可选的各种警告

关于将更改归并到主程序

  • 你是否将编译程序的警告(包括可选)都处理了
  • 你的代码是否未用Lint
  • 你的代码进行了单元测试吗
  • 你是否逐步通过了每一条编码路径以观察数据流
  • 你是否逐步通过了汇编语言层次上的所有关键代码
  • 是否清理过了任何代码,如果是,修改出经过了彻底测试了吗?
  • 文档是否指出了使用你的代码有危险之处吗?
  • 程序维护人员是否能够理解你的代码

每当实现了一个函数或子系统之时

  • 是否用断言证实了函数参数的有效性
  • 代码中是否有了未定义的或者无意义的代码
  • 代码是否能创建未定义的数据
  • 有没有难以理解的断言,对它们作解释了没有
  • 你在代码中是否作过任何假设
  • 是否使用断言警告可能出现的非常情况
  • 是否做过防御性程序设计?代码是否隐藏了错误?
  • 是否用第二个算法来验证第一个算法
  • 是否可由用于确认代码或数据的启动检查
  • 代码是否包含了随机行为?能消除这些行为吗?
  • 你的代码如产生了无用信息,你是否在DEBUG代码中也把它们置为无用信息
  • 代码中是否有稀奇古怪的行为
  • 若代码是子系统的一部分,那么你是否建立一个子系统测试
  • 在你的设计和代码中是否有任意情况
  • 即使程序员不感到需要,你也做完整性检查吗
  • 你是否因为排错程序太大或太慢,而将有价值的DEBUG测试抛置一边
  • 是否使用了不可移植的数据类型
  • 代码中是否有变量或表达式产生上溢或下溢
  • 是否准确地实现了你的设计,还是非常近似地实现了你的设计
  • 代码是否不止一次地解同一问题
  • 是否企图消除代码中的每一个if语句
  • 是否用过嵌套?: 运算符
  • 是否已将专用代码孤立出来
  • 是否用到了由风险的语言惯用语
  • 是否不必要地将不同类型的运算符混用
  • 是否调用了返回错误的函数?你能消除这种调用吗
  • 是否引用了尚未分配的存储空间
  • 是否引用已经释放了的存储空间
  • 是否不必要地多用了输出缓冲存储
  • 是否向静态或全局缓冲区传送了数据
  • 你的函数是否依赖于另一个函数的内部细节
  • 是否使用了怪异的或有疑问的C惯用语
  • 在代码中是否有挤在一行的毛病
  • 代码有不必要所谓灵活性,你能消除它们吗
  • 你的代码是经过多次“试着”求解的结果吗
  • 函数是否小并容易测试

每当设计了一个函数或子系统后

  • 此特征是否符合产品的市场策略
  • 错误代码是否作为正常返回值得特殊情况而隐藏起来
  • 是否评审了你的界面,它能保证难于出现误操作?
  • 是否具体多用途且面面俱到的函数
  • 你是否有太灵活的(空空洞洞的)函数参数
  • 当你的函数不再需要时,它是否返回一个错误条件
  • 在调用点你的函数是否易读
  • 你的函数是否有布尔变量输入,避免它,将特殊的情况分成两个函数

修改错误之时

  • 错误无法消失,是否能找到错误的根源
  • 是修改了错误的真正根源,还是仅仅修改了错误的症状

10. 总结

决不允许同样错误出现两次