为什么“以循环索引为猴子”不好?

史蒂夫麦康奈尔的一个清单项目是你不应该使用循环索引 (第16章,第25页, 循环索引 ,PDF格式)。

这是直觉上的,并且是我一直遵循的练习,除非我学会了如何在当天编程。

在最近的一次代码审查中,我发现这个尴尬的循环并立即将其标记为可疑。

for ( int i=0 ; i < this.MyControl.TabPages.Count ; i++ ) { this.MyControl.TabPages.Remove ( this.MyControl.TabPages[i] ); i--; } 

它几乎是有趣的,因为它设法通过将索引保持为零来工作,直到删除所有TabPages。

这个循环可以写成

  while(MyControl.TabPages.Count > 0) MyControl.TabPages.RemoveAt(0); 

而且由于控制实际上与循环几乎同时写入,它甚至可以写成

  MyControl.TabPages.Clear(); 

从那以后,我一直受到代码审查问题的挑战,并发现我对错误实践的阐述并不像我所希望的那样强烈。 我说很难理解循环的流程,因此难以维护和调试,并且最终在代码的生命周期内更加昂贵。

是否更好地阐明了为什么这是不好的做法?

我认为你的表达很棒。 也许它可以像这样措辞:

由于逻辑可以表达得更清楚,它应该。

好吧,这会增加混乱的目的 – 你可以很容易地写:

 while(MyControl.TabPages.Count > 0) { MyControl.TabPages.Remove(MyControl.TabPages[0]); } 

或(更简单)

 while(MyControl.TabPages.Count > 0) { MyControl.TabPages.RemoveAt(0); } 

或(最简单)

 MyControl.TabPages.Clear(); 

在上述所有情况中,我不必眯眼并考虑任何边缘情况; 很清楚会发生什么事情。 如果要修改循环索引,可以快速使其难以理解。

这都是关于期望的。

当使用循环计数器时,您期望它以相同的量递增(递减)循环的每次迭代。

如果你用循环计数器弄乱(或者你喜欢猴子),你的循环就不会像预期的那样。 这意味着它更难理解,并且增加了代码被误解的可能性,并且这引入了错误。 或者(误)引用一个聪明但虚构的角色:

 complexity leads to misunderstanding misunderstanding leads to bugs bugs leads to the dark side. 

我同意你的挑战。 如果他们想要保持for循环,代码:

 for ( int i=0 ; i < this.MyControl.TabPages.Count ; i++ ) { this.MyControl.TabPages.Remove ( this.MyControl.TabPages[i] ); i--; } 

减少如下:

 for ( int i=0 ; i < this.MyControl.TabPages.Count ; ) { this.MyControl.TabPages.Remove ( this.MyControl.TabPages[i] ); } 

然后:

 for ( ; 0 < this.MyControl.TabPages.Count ; ) { this.MyControl.TabPages.Remove ( this.MyControl.TabPages[0] ); } 

但是,如果存在,则while循环或Clear()方法显然是可取的。

我认为你可以通过调用Knuth的文字编程概念来构建一个更强大的论点, 应该为计算机编写程序,而是将概念传递给其他程序员 ,因此更简单的循环:

  while (this.MyControl.TabPages.Count>0) { this.MyControl.TabPages.Remove ( this.MyControl.TabPages[0] ); } 

更清楚地说明了意图 – 删除第一个标签页,直到没有剩下。 我想大多数人会比原来的例子快得多。

这可能更清楚:

 while (this.MyControl.TabPages.Count > 0) { this.MyControl.TabPages.Remove ( this.MyControl.TabPages[0] ); } 

可以使用的一个参数是调试这样的代码要困难得多,其中索引被更改两次。

原始代码是高度冗余的,可以将for循环的动作弯曲到必要的位置。 增量是不必要的,并且通过减量来平衡。 这些应该是PRE增量,而不是POST增量,因为从概念上讲,后增量是错误的。 与tabpages计数的比较是半冗余的,因为这是检查容器是空的一种hackish方式。

简而言之,它是不必要的聪明,它增加而不是消除冗余。 既然它既明显更简单又明显更短,那就错了。

根本没有索引索引的唯一理由是,如果有人选择性地擦除事物。 即使在那种情况下,我认为最好说:

  I = 0;
   while(i 

而不是每次删除后jinxing循环索引。 或者,更好的是,将索引向下计数,以便在删除项目时,它不会影响将来需要删除的项目的索引。 如果删除了许多项目,这也可以帮助最大限度地减少每次删除后移动项目所花费的时间。

我不确定我是否完全理解你所追求的是什么,但也许是这样的?

 for ( int i=this.MyControl.TabPages.Count - 1 ; i >= 0 ; i-- ) { this.MyControl.TabPages.Remove ( this.MyControl.TabPages[i] ); } 

或者只是TabPages集合的实现IList:

  this.MyControl.TabPages.Clear(); 

我认为指出循环迭代不是由任何人所期望的“i ++”控制的事实,而是疯狂的“i–”设置应该已经足够了。

我还认为通过评估计数然后改变循环中的计数来改变“i”的状态也可能导致潜在的问题。 我希望for循环通常具有“固定”迭代次数,并且for循环条件的唯一部分变为循环变量“i”。