从达夫设备(Duff's Device)说代码可读性
刚才在一个IT新闻站上看到类似如下的C代码引用,有人就说可读性差,我想估计不少从业者在接触代码可读性这个概念之后,也会认同这个说法,其实这种看法是值得商榷的。
register n = (count + 7) / 8;
switch (count % 8)
{
case 0: do { *to = *from++;
case 7: *to = *from++;
case 6: *to = *from++;
case 5: *to = *from++;
case 4: *to = *from++;
case 3: *to = *from++;
case 2: *to = *from++;
case 1: *to = *from++;
} while (--n > 0);
}
(这段代码是抄来的,在这里估摸着to的地址上是一个设备,所以to的值没有变化)
这玩意有个名字,就是Duff's Device,看起来像游戏里某个传说中的魔法师的发明神马的 :)。说实话,之前我没有用过这种把do { ... } while(...)嵌进switch的做法,如果我写这段代码,我会怎么写呢?一个可能的写法是:
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
register n = count / 8;
switch (count % 8)
{
case 7: *to = *from++;
case 6: *to = *from++;
case 5: *to = *from++;
case 4: *to = *from++;
case 3: *to = *from++;
case 2: *to = *from++;
case 1: *to = *from++;
}
for( ; n > 0; n--){
*to = *from++;
*to = *from++;
*to = *from++;
*to = *from++;
*to = *from++;
*to = *from++;
*to = *from++;
*to = *from++;
}
想必这个版本就比较清晰了。连续多行的*to = *from++,是为了以某个倍数(这里是8)减少循环(及其附带操作)的次数(由count次变为n次),提高效率;switch的作用实质上就是为了处理倍数的余数:因为没有break所以case到哪里,就会从那一行开始连续执行下去。
知道这两个版本在干吗,首先是要知道这种经典的循环展开优化;知道第一个版本在干吗,关键是搞明白do { ... } while(...)嵌套在switch中会发生什么:do中的内容第一次从哪儿开始由switch决定;而在循环时是顺序执行的,因为switch不在这个block里发挥作用了。
有人可能觉得第一个版本是奇技淫巧,我不这么看。首先这虽然比起一般的代码晦涩一点点,但并非不能理清楚语法间的关系。其次,它利用语法上的可行性,使得代码行数得以缩短,又不损失任何效率。换句话说,只要我们搞懂了这个用法(一次性付出),所有类似代码的写和读反而成本降低了(多次付出)。
个人觉得,所谓代码可读性,不是以损失语言语法或某个编译器提供的可行性为代价的。可读性主要衡量的是在整个项目中,各个部分的交互的复杂程度:因为当我们看着一段代码的时候,还要想清楚整个项目的图景,若是在各个部分间存在繁复的相互关系,是比较困难的。
至于眼前不到一个屏幕的代码若是还不能理解,恐怕更好的办法不是让代码作者去修改代码,而是让代码读者稍微多掌握一点关于这个用法的知识,不论这个知识是否偏门。总而言之,不要把代码可读性当作自己看不懂一些小技巧的借口就对了。
P.S. 从另一方面讲,这种优化(无论什么版本)简单、机械,实际上应该由机器而不是人来完成,不过这就是另外一个话题了。