这是一本老书,作者 Steve Maguire 在微软工作期间写了这本书,英文版于 1993 年发布。2013 年推出了 20 周年纪念第二版。我们看到的标题是中译版名字,英文版的名字是《Writing Clean Code ─── Microsoft’s Techniques for Developing》,这本书主要讨论如何编写健壮、高质量的代码。作者在书中分享了许多实际编程的技巧和经验,旨在帮助开发人员避免常见的编程错误,提高代码的可靠性和可维护性。
不记录,等于没读。本文记录书中附录 A:编码检查表。
这里给出的问题列表,总结了书中的所有观点。
读者需要持续的建立自己的准则列表,并决不允许同样的错误出现两次。每当在项目中发现 BUG 后,都要问问自己:
如何检测出这个 BUG?
如何预防这个 BUG?
然后,将得到的启示加入自己的准则列表中去。
为了提醒你回顾本书最重要的知识点,我创建了几个核查表。你可以在自己的开发中回顾这些知识点:设计
、编码实现
、为调试做准备
、测试
和 在线调试
。我假设你已经满足以下条件:
- 使用优秀的编译器,开启所有警告输出;
- 维护一个 DEBUG 版本;
- 立刻修复 BUG;
为了有效的使用核查表,在每次向项目新增代码时你都要检查它们。实际上,“每次”代表的是“接下来的几次”新增代码。再之后,你应该对背离、破坏准则的代码有一种第六感,也就是能闻得到坏代码的味道。
设计
-
设计包含未定义或无意义的行为吗?那么随机或罕见的行为呢?
设计是否允许不必要的灵活性或基于不必要的假设吗?
设计中有未经仔细思考的细节吗?
-
设计中用到静态或全局缓冲区传递数据吗?
函数依赖其它函数的内部实现细节吗?
-
设计需要处理任何特殊情况吗?是否隔离了这些特殊情况代码?
-
查看函数的输入和输出:
- 每个输入和输出都只表示一种类型的数据吗?
- 会包含错误值或其它难以注意的值吗?
健壮的接口确保每个输入和输出都是清晰的,以便程序员不能忽视重要的细节。
malloc
函数返回NULL
错误值、realloc
可以传递大小为0的参数值,这些都不是健壮的接口。 -
预判程序员如何调用你的函数。“显而易见”的方法是否正确?
-
站在维护者的角度,你的函数在调用点具有可读性吗?函数的参数能清晰的表明调用的意图吗?
一个函数只做一件事吗?如果函数接受
TRUE
和FALSE
参数,常常表明函数做了多件事情,这可不是好设计。 -
你的函数返回错误值吗?重新定义这些函数可以消除错误条件吗?要知道,如果函数返回错误,必须要在每个调用点处理错误。
-
最重要的是,你使用单元测试自动地、彻底地验证设计吗?如果没有,你必须考虑可测试的替代方案。
编码实现
-
对比设计和编码实现:你是否准确地实现了设计?设计和实现之间的微小差异可能会使你陷入困境。
-
代码中是否有不必要的假设?是否使用了不可移植的数据类型?
-
检查你代码中的表达式。它们是否有上溢或下溢?变量呢?
-
是否使用危险的C语言惯用语,比如嵌套的
?:
操作符或者用移位实现除法?代码中混用位操作和算术操作,并且没有充分理由吗
你有使用危险的 C 惯用语吗?比如在算术环境中使用逻辑表达式的结果 0 和 1。
-
仔细查看你的代码。是有团队中一般程序员都无法理解的炫技型 C 代码?考虑使用主流 C 重写代码。
-
可能一个函数只做了一件事,但是这件事是用单一路径完成的吗?是否使用不同的代码实现了不同的特殊情况?要尽量消除代码中的每个
if
语句。 -
是否调用返回错误的函数?你能修改设计不再调用吗?这样就可以消除错误处理代码。
-
是否引用了无权访问的内存?特别是,是否引用了已经释放的内存?
是否读取过其它子系统的私有数据结构?
-
如果你的函数使用指向输入或输出的指针,你的代码是否将其引用限制为仅保留这些输入和输出所需的内存?如果没有,你的代码可能基于了某些不正确的假设,比如假设调用者为数据分配了能保存最大数值的内存。
增加调试支持
发现代码中的隐藏 BUG 并不容易,向项目中增加断言和其它调试代码可以减少发现隐藏 BUG 所需的时间。
- 是否使用断言验证函数参数?当因为没有足够的信息,你不能验证某个特殊参数时,那么维护额外的调试信息会有帮助吗?回想一下仅用于调试的
sizeofBlock
函数,对于验证指向已分配内存的指针所起到的帮助。 - 是否使用断言验证你的假设,或检测未定义行为的非法用法。
- 防御性编程在“修复”内部 BUG 的同时,是否也掩盖了错误?你是否在 DEBUG 版本中使用断言检查这些 BUG。对于对外接口(比如用户输入数据),这条并不适用,对外部接口的防御,应该是防弹的。
- 断言清晰吗?如果不清晰,给断言增加详细注释。当断言捕获一个错误时,如果程序员看不懂断言的目的,他常常会假定这个断言是无效的,然后删除掉断言。注释可以保护你的断言。
- 如果你的代码使用动态分配内存,你会使用仅用于调试的代码将未初始化内容设置为已知的特殊值吗?将内存设置为特殊值,可以更容易的发现和重现使用未初始化内存的 BUG。
- 如果你的代码释放内存,在释放前先破坏内存内容了吗?
- 你的代码有重要的算法吗?你会再写一个完全不同的、只用于调试的算法去检查主算法吗?
- 为了尽早检测出 BUG,是否可以在程序启动时进行调试检查?比如对数据表的检测。
测试
程序员测试自己的代码至关重要,即使为此推迟交付也在所不惜。
- 代码编译后是否不生成任何警告信息?如果你使用
lint
或者类似诊断工具,你的代码能通过所有测试吗?你的代码能通过单元测试吗?如果你跳过这些步骤中的任何一个,你就失去一个更早检测到 BUG 的机会。 - 你是否使用调试器对新代码进行逐条跟踪,不仅专注于代码,还关注流经该代码的数据?这可能是在代码中捕获 BUG 的最好方法。
- 你是否“清理”任何代码?如果是,你对此进行测试了吗?是否对这些代码进行逐条跟踪?要记住,清理代码和写新代码一样必须进行彻底的测试。
- 为新代码编写单元测试了吗?
调试
- 你能重现测试组报告的 BUG 吗?如果不能,记住 BUG 不会自己修复。要么隐藏起来了,要么已经修复了。为了确定是那种情况,你需要使用测出BUG的程序版本进行测试。
- 你发现 BUG 的真正原因了吗?还是仅仅发现 BUG 的表面症状?确保跟踪到 BUG 的真正原因。
- 这个 BUG 如何预防呢?提出一个可以预防此 BUG的 精确指南。
- 这个 BUG 如何自动检测呢?使用断言能捕获它吗?使用某些调试代码呢?编码习惯和流程的哪些变化会对此有帮助呢?
每一份打赏,都是对创作者劳动的肯定与回报。!