翻译《有关编程、重构及其他的终极问题?》——13.表格化的格式化
2017-01-03 20:42
429 查看
翻译《有关编程、重构及其他的终极问题?》——13.表格化的格式化
标签(空格分隔): 翻译 技术 C/C++作者:Andrey Karpov
翻译者:顾笑群 - Rafael Gu
最后更新:2017年01月03日
13.表格化的格式化
下面这段代码摘自ReactOS项目(和Windows兼容的开源操作系统)。PVS-Studio诊断出来的错误描述为:V560 A part of conditional expression is always true: 10035L(译者注:大意是部分条件判断始终为true)。void adns__querysend_tcp(adns_query qu, struct timeval now) { ... if (!(errno == EAGAIN || EWOULDBLOCK || errno == EINTR || errno == ENOSPC || errno == ENOBUFS || errno == ENOMEM)) { ... }
解释
上面的代码很少,所以你能很容易发现错误的地方。但当我们处理现实中真实代码时,却是很难发现bug。当阅读到类似前述代码时,你一般都很可能无意识忽略掉类似的比较块而跳到下一段代码去了。
造成这种情况的主要原因是这些条件判断几乎没有被格式化,而且因为需要一些额外的精力,所以你也不会花费太多的注意力在上面——我们一般会潜意识的认为既然那些条件检查都是类似的,那么其中就应该不会有错误,每个判断都是好的。
避免这个错误一个方法是把代码进行表格化的格式化。
如果你因为太懒而没有发现上面代码的错误,我告诉你,其实是在检查中缺少了一个“errno ==”。因为EWOULDBLOCK一直是true,所以将导致这个条件判断一直未true。
正确的代码
if (!(errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR || errno == ENOSPC || errno == ENOBUFS || errno == ENOMEM)) {
建议
作为一个开始,这里我放一个表格化格式化的代码版本。当然,我其实并不喜欢这种。
if (!(errno == EAGAIN || EWOULDBLOCK || errno == EINTR || errno == ENOSPC || errno == ENOBUFS || errno == ENOMEM)) {
这看上去是好了一点,但其实还可以更好。
因为两个原因,我不喜欢这种布局。首先,错误还不是太明显;第二,你不得不插入不少空格去对齐代码。
这时我们就需要采用两种方法去改进这个格式化。第一个方式就是我们每行不要包含超过一个比较:这样就可以让错误更容易发现。比如:
a == 1 && b == 2 && c && d == 3 &&
第二个办法就是用更合理的方式写&&,||等操作符,比如放在左边对齐。
让我们看看用空格来对齐代码,太沉闷了:
x == a && y == bbbbb && z == cccccccccc &&
但当我们左对齐操作符时,一切就变得更快和更简单:
x == a && y == bbbbb && z == cccccccccc
上面的代码虽然有一些奇怪,但你会很快适应它们。
让我们把这两种方式在我们前面的示例代码中合并在一起:
if (!( errno == EAGAIN || EWOULDBLOCK || errno == EINTR || errno == ENOSPC || errno == ENOBUFS || errno == ENOMEM)) {
是的,现在代码变长了——但是错误也很容易被发现了。
我同意这看上去有些奇怪,然而,其实我建议使用的是另外一种技术。我已经使用这种技术一年半了,我非常享受它,所以我对我的推荐很有信息。
我从来没有发现代码变长会有什么不好的。所以我曾经这么写这段代码:
const bool error = errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR || errno == ENOSPC || errno == ENOBUFS || errno == ENOMEM; if (!error) {
是不是因为代码变得太长和混乱而有些失望?我同意。所以让我们再使用一个函数!
static bool IsInterestingError(int errno) { return errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR || errno == ENOSPC || errno == ENOBUFS || errno == ENOMEM; } .... if (!IsInterestingError(errno)) {
也许你认为我所做的有点太完美主义了。但是我能确信,在复杂的表达式中错误是经常发生的,而我又不能每次都能把它们找出来——它们可能在任何地方,而且它们可能很难去发现(译者注:作者有些内疚的为太长的代码进行辩解)。
这里有另外一个WinDjView项目中的例子:
inline bool IsValidChar(int c) { return c == 0x9 || 0xA || c == 0xD || c >= 0x20 && c <= 0xD7FF || c >= 0xE000 && c <= 0xFFFD || c >= 0x10000 && c <= 0x10FFFF; }
这个函数只包含了少数几行,但它其中还是有错误的。这个函数也会一直返回true。主要原因还是因为几乎没有对代码进行格式化(译者注:所以很难发现错误),而且维护这些代码很多年的程序员也不会主动去认真的读它们。
让我们把这段代码重构为表格化的,我另外还会增加一些括号:
inline bool IsValidChar(int c) { return c == 0x9 || 0xA || c == 0xD || (c >= 0x20 && c <= 0xD7FF) || (c >= 0xE000 && c <= 0xFFFD) || (c >= 0x10000 && c <= 0x10FFFF); }
你不必把你的代码格式化成和我建议的一模一样。这一篇文章的目的是让你注意在混乱的代码中的输入错误。通过吧代码表格化,你能避免很多愚蠢的输入错误,真的很有效。所以,我希望这篇文章能帮到你。
注意
坦白说,我不得不警告你,表格化的格式化代码某些时候也会引起伤害。看看下面这个例子:
inline void elxLuminocity(const PixelRGBi& iPixel, LuminanceCell< PixelRGBi >& oCell) { oCell._luminance = 2220*iPixel._red + 7067*iPixel._blue + 0713*iPixel._green; oCell._pixel = iPixel; }
这是从eLync SDK项目中拿出的代码。程序员想对齐代码,所以他在713前增加了0。不幸的的是,他忘记了以0为开头的数字一般意味着这个数字是8进制的(译者注:在C系语言中,这个定义很普遍)。
字符串数组
我想我已经把表格化的代码格式化表述的已经够清楚了,但我最好还是多给些例子。让我们再多看一个例子。顺便说一下,表格化的代码格式化不光可以用在条件判断上,还可以用在一个语言的其他各种构造上。
下面这段代码是Asterisk项目里的。PVS-Studio诊断的错误说明为:V653 A suspicious string consisting of two parts is used for array initialization. It is possible that a comma is missing. Consider inspecting this literal: “KW_INCLUDES” “KW_JUMP”(译者注:大意是说有一个字符串很可疑,很可能是其实是两个字符串,只是缺少了逗号,可以的字符串是“KW_INCLUDES”和“KW_JUMP”)。
static char *token_equivs1[] = { .... "KW_IF", "KW_IGNOREPAT", "KW_INCLUDES" "KW_JUMP", "KW_MACRO", "KW_PATTERN", .... };
这里其实有一个输入错误——一个逗号被遗漏了。结果两个完全不同的字符串被整合成了一个,比如,我们实际得到的是:
.... "KW_INCLUDESKW_JUMP", ....
如果写这段代码的程序员使用了表格化格式化,那么这个错误是可以避免的。看下面的代码,如果逗号遗漏了,那将会很容易被发现:
static char *token_equivs1[] = { .... "KW_IF" , "KW_IGNOREPAT" , "KW_INCLUDES" , "KW_JUMP" , "KW_MACRO" , "KW_PATTERN" , .... };
和前面提到的类似,请注意,如果我们把分割符号放在右边(这里是一个逗号),你不得不增加很多空格,这有点麻烦。而且,当我们有一个新的更长的行要加入时,我们不得不要重新进行表格格式化,这台不方便了。
这也是我们为何我再次建议使用如下方式进行表格化格式化:
static char *token_equivs1[] = { .... , "KW_IF" , "KW_IGNOREPAT" , "KW_INCLUDES" , "KW_JUMP" , "KW_MACRO" , "KW_PATTERN" .... };
现在依旧可以很容易的找到一楼的逗号,而且我们还不需要很多空格——代码也看上去很漂亮和直观。也许这种格式化的方式不是太常见,但你能很快适应它——亲自尝试一下吧。
相关文章推荐
- 翻译《有关编程、重构及其他的终极问题?》——4.小心--操作符,请把表达式放在括号中
- 翻译《有关编程、重构及其他的终极问题?》——9.使用'-0'符号作为结尾标记
- 翻译《有关编程、重构及其他的终极问题?》——22.不要使用#pragram warning(default-X)
- 翻译《有关编程、重构及其他的终极问题?》——28.如果你可以使用简单的函数就不要使用宏
- 翻译《有关编程、重构及其他的终极问题?》——24.override和final关键字应该成为你的新朋友
- 翻译《有关编程、重构及其他的终极问题?》——5.使用工具去分析你的代码
- 翻译《有关编程、重构及其他的终极问题?》——前言
- 翻译《有关编程、重构及其他的终极问题?》——19.如何合理的从一个构造函数中调用另外一个构造函数
- 翻译《有关编程、重构及其他的终极问题?》——27.狡猾的BSTR字符串
- 翻译《有关编程、重构及其他的终极问题?》——7.不要在循环中调用alloca()函数
- 翻译《有关编程、重构及其他的终极问题?》——16.在编程过程中“装逼”是不可接受的
- 翻译《有关编程、重构及其他的终极问题?》——11.不要试图把尽量多的操作符放到一行代码里
- 翻译《有关编程、重构及其他的终极问题?》——23.自动获取字符串的长度
- 翻译《有关编程、重构及其他的终极问题?》——2.比0大的并不意味着就只是1
- 翻译《有关编程、重构及其他的终极问题?》——8.记住:析构函数中的异常是危险的
- 翻译《有关编程、重构及其他的终极问题?》——14.一个好的编译器和代码风格还不够
- 翻译《有关编程、重构及其他的终极问题?》——15.在你的代码中开始使用enum class吧
- 翻译《有关编程、重构及其他的终极问题?》——25.不要再用this指针和nullptr比较了
- 电子书《有关编程、重构及其他的终极问题?》的翻译
- 翻译《有关编程、重构及其他的终极问题?》——21.正确的检查文件的结尾符(EOF)