翻译《有关编程、重构及其他的终极问题?》——13.表格化的格式化

翻译 2017年01月03日 20:42:29

翻译《有关编程、重构及其他的终极问题?》——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"
  ....
};

现在依旧可以很容易的找到一楼的逗号,而且我们还不需要很多空格——代码也看上去很漂亮和直观。也许这种格式化的方式不是太常见,但你能很快适应它——亲自尝试一下吧。

翻译《有关编程、重构及其他的终极问题?》——8.记住:析构函数中的异常是危险的

对于异常,我在C++和Java中用的都不多,现在ES6的Node.js中因为使用了generator、Promise和co的同步化异步操作,不得不使用异常,但这个异常和传统的异常不一致。传统异常,特别...
  • headman
  • headman
  • 2016年12月08日 21:48
  • 205

翻译《有关编程、重构及其他的终极问题?》——12.当使用拷贝黏贴,一定要特别注意最后一行

的确,当连续的代码块大部分类似时,我们会使用复制黏贴来加快编码速度,然后再手动修改每一块不一样的地方,但是——我的经验也是——我们会在最后一行犯错误。作者还严谨的做了统计,不止你、不止我,而是大部分程...
  • headman
  • headman
  • 2016年12月23日 21:20
  • 267

翻译《有关编程、重构及其他的终极问题?》——2.比0大的并不意味着就只是1

当不相同时,memcmp()函数返回大于0或小于0,而非1或-1,就是这么简单的错误,导致了MySQL/MariaDB前几年的主要几个发布版本都出现了安全漏洞,即黑客无须知道密码,就可以获得root管...
  • headman
  • headman
  • 2016年11月13日 10:20
  • 282

翻译《有关编程、重构及其他的终极问题?》——15.在你的代码中开始使用enum class吧

C++ 11中有很多新特性,绝大多数特性带来了便利的同时,也或多或少的带来了新的问题,比如auto关键字,但enum class绝对是个好东西,使得原来编译器不做类型检查的枚举类型,有了强类型检查的属...
  • headman
  • headman
  • 2017年02月01日 11:27
  • 116

翻译《有关编程、重构及其他的终极问题?》——23.自动获取字符串的长度

这应该算是C/C++的福气呢?还是缺点呢?计算字符串的长度都有这么多文章。我觉得看具体情况吧。至少C/C++给了程序员最大程度的可控空间以及各种各样的手段。...
  • headman
  • headman
  • 2017年04月14日 13:45
  • 225

翻译《有关编程、重构及其他的终极问题?》——1. 别把编译器的事给做了

所以,我的建议是——写简单且易读的代码。这里有个规则:简单的代码通常就是正确的代码。不要尝试去做编译器的活——比如去展开循环。编译器无须你的帮助,就可以把绝大多数事情做好。这样的人工优化工作一般只有在...
  • headman
  • headman
  • 2016年11月12日 20:40
  • 366

翻译《有关编程、重构及其他的终极问题?》——28.如果你可以使用简单的函数就不要使用宏

在C/C++中,还是要减少对宏的依赖,虽然宏有时候也很有用。
  • headman
  • headman
  • 2017年07月13日 17:51
  • 99

翻译《有关编程、重构及其他的终极问题?》——29.在迭代器上请使用前置自增操作符(++i),不要使用后置自增操作符(i++)

很多C/C++程序员都知道++i和i++的区别,但在迭代器上,这个区别就更大了,甚至到了影响性能的地步。...
  • headman
  • headman
  • 2017年07月15日 10:03
  • 267

翻译《有关编程、重构及其他的终极问题?》——5.使用工具去分析你的代码

如果遇到有些编程问题需要详细的了解相关文档的每一个部分,才能避免问题怎么办?去看文档吗?sorry,你得花费很多时间去看,这样就不能保证你有足够的时间去完成编程了。只有一个建议,就是直接用工具,比如P...
  • headman
  • headman
  • 2016年12月04日 20:10
  • 262

翻译《有关编程、重构及其他的终极问题?》——3.复制一次,检查两次

凡事预则立,类似因为使用复制代码产生的问题,相当多都很难发现,一般只有通过code review或者debug才能发现,因为一般编译器都不会报错,然后其实只有一个字母或者数字错了。PSV-Studio...
  • headman
  • headman
  • 2016年11月20日 21:45
  • 302
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:翻译《有关编程、重构及其他的终极问题?》——13.表格化的格式化
举报原因:
原因补充:

(最多只允许输入30个字)