循环中的最后一个元素是否应该单独处理?
审核时,有时会遇到这种循环:
i = begin while ( i != end ) { // ... do stuff if ( i == end-1 (the one-but-last element) ) { ... do other stuff } increment i }
然后我问一个问题:你会写这个吗?
i = begin mid = ( end - begin ) / 2 // (the middle element) while ( i != end ) { // ... do stuff if ( i > mid ) { ... do other stuff } increment i }
在我看来,这超出了编写循环的意图:之所以循环,是因为每个元素都需要做一些共同的事情。使用此构造,对于某些元素,我们可以做一些不同的事情。因此,我得出结论,我们需要为这些元素单独循环:
i = begin mid = ( end - begin ) / 2 //(the middle element) while ( i != mid ) { // ... do stuff increment i } while ( i != end ) { // ... do stuff // ... do other stuff increment i }
现在我什至在SO上看到了一个关于如何以一种很好的方式编写if子句的问题……我很伤心:这里有些不对劲。
我错了吗?如果是这样,那么在编码时用特例弄乱循环主体有什么好处,我们已经预先意识到这些特例?
解决方案
当然,可以将其拉出的特殊包装物品是很愚蠢的。我也不会重复do_stuff;我将其放在函数或者宏中,所以我不会复制粘贴代码。
哪一个表现更好?
如果项目数量很大,那么我将总是循环一次,特别是如果我们要对每个项目执行某些操作。评估条件的成本可能少于两次循环。
糟糕,我们当然不会循环两次...在这种情况下,最好使用两个循环。但是,我认为主要考虑因素应该是性能。如果我们可以通过对循环范围的简单操作(一次)对工作进行分区,则无需在循环中产生条件(N次)。
我知道当人们尝试将数组的元素连接成逗号分隔的字符串时,我已经看到了这一点:
for(i=0;i<elements.size;i++) { if (i>0) { string += ',' } string += elements[i] }
我们可以在其中具有if子句,或者必须在末尾再次复制字符串+ =行。
在这种情况下,显而易见的解决方案是
string = elements.join(',')
但是join方法在内部执行相同的循环。并非总是有一种方法可以做我们想要的。
我意识到,当我将特殊情况放在for循环中时,我通常出于自己的利益而变得太聪明了。
我认为循环应该平等地处理所有元素是正确的。不幸的是,有时还是会有一些特殊情况,这些特殊情况应通过if语句在循环结构中处理。
如果有很多特殊情况,我们可能应该考虑采用某种方法在单独的结构中处理两组不同的元素。
我不希望看到的另一件事是for-case模式:
for (i=0; i<5; i++) { switch(i) { case 0: // something break; case 1: // something else break; // etc... } }
我已经在真实代码中看到了这一点。
@xtofl,
我同意你的担心。
百万次我遇到类似的问题。
开发人员会为第一个元素或者最后一个元素添加特殊处理。
在大多数情况下,值得从startIdx + 1或者endIdx 1元素循环,甚至将一个长循环分成多个较短的循环。
在极少数情况下,不可能拆分循环。
我认为,不常见的事情应尽可能在循环之外进行处理。
我认为这个问题不应该由一个原则来回答(例如,"循环地,平等对待每个元素")。相反,我们可以查看两个因素来评估实现的好坏:
- 运行时有效性-编译后的代码运行速度是否很快,或者以不同的方式运行会更快?
- 代码的可维护性-(对于另一个开发人员)容易理解这里发生的事情吗?
如果更快,并且通过一次循环执行所有代码来提高代码的可读性,请按照这种方式进行操作。如果速度较慢且可读性较差,请以其他方式进行。
如果它更快,更不易读,或者更慢但更易读,请找出在特定情况下哪些因素更为重要,然后决定如何循环(或者不循环)。
在我们发布的最后一个代码段中,我们正在重复// ....做事的代码。
当我们在一组不同的索引上具有完全不同的一组操作时,保留2个循环是有意义的。
i = begin mid = ( end - begin ) / 2 //(the middle element) while ( i != mid ) { // ... do stuff increment i } while ( i != end ) { // ... do other stuff increment i }
并非如此,我们仍然希望保留一个循环。但是事实仍然是我们仍然保存(结束开始)/ 2比较次数。因此,归结为我们是希望代码看起来简洁还是要节省一些CPU周期。电话是你的。
如果只需要执行一次,则特殊情况应在循环外部完成。
但是,由于作用域的原因,可能存在一个索引或者一些其他变量,它们更容易保留在循环中。将数据结构上的所有操作都保留在循环控制结构中可能还有一个上下文相关的原因,尽管我认为这本身是一个较弱的论点。
它只是根据需要和便利使用它。因此,没有提及平等对待元素,并且肯定不会损害语言所提供的功能。
我认为我们已经完全钉牢了。当大多数人可以在外部执行条件分支时,他们陷入了将条件分支包括在循环中的陷阱:这只是更快。
例如:
if(items == null) return null; StringBuilder result = new StringBuilder(); if(items.Length != 0) { result.Append(items[0]); // Special case outside loop. for(int i = 1; i < items.Length; i++) // Note: we start at element one. { result.Append(";"); result.Append(items[i]); } } return result.ToString();
我们描述的中间情况简直令人讨厌。试想一下,如果代码增长了,需要将其重构为不同的方法。
除非我们正在解析XML,否则<grin>循环应尽可能简单明了。