循环终止条件
这些" for"循环是算法形式正确性证明的第一个基本示例。它们具有不同但等效的终止条件:
1 for ( int i = 0; i != N; ++i ) 2 for ( int i = 0; i < N; ++i )
在后置条件中,区别变得很明显:
- 第一个强有力地保证了循环终止后," i == N"。
- 第二个仅给出了微弱的保证,即在循环终止后," i> = N",但是我们会倾向于假设" i == N"。
如果由于某种原因将增量++ i更改为类似i + = 2的内容,或者如果在循环内修改了i,或者如果N为负,则程序可能会失败:
- 第一个可能陷入无限循环。它在出现错误的循环中尽早失败。调试很容易。
- 第二个循环将终止,并且在稍后的某个时间,由于我们对i == N的错误假设,程序可能会失败。它可能会在远离导致该错误的循环的远处失败,从而使其难以追溯。或者它可以默默地继续做一些意想不到的事情,甚至更糟。
我们更喜欢哪种终止条件,为什么?还有其他考虑事项吗?为什么许多知道这一点的程序员拒绝应用它?
解决方案
在C ++中,出于普遍考虑,首选使用!=
测试。 C ++中的迭代器具有各种概念,例如输入迭代器,正向迭代器,双向迭代器,随机访问迭代器,每一个都以新功能扩展了前一个迭代器。为了使<<起作用,需要随机访问迭代器,而!=
仅需要输入迭代器。
如果由于某种原因有人改变了计数器的递增方式,而他们又会改变终止条件和结果逻辑(如果i == N是必需的话),那么我们不应孤立地看待计数器。
我更喜欢第二种条件,因为它更标准,不会导致无限循环。
如果我们信任自己的代码,则可以执行任何一种操作。
如果我们希望代码易于阅读且易于理解(因此可以更宽容地接受假设为klutz的用户的更改),则可以使用类似的方法;
for ( int i = 0 ; i >= 0 && i < N ; ++i)
我总是使用#2,因为这样我们就可以确定循环会终止...后来依靠它等于N依赖于副作用...我们不是会更好地使用变量N本身吗?
[编辑]对不起...我的意思是#2
总的来说,我更喜欢
for ( int i = 0; i < N; ++i )
生产中有错误的程序的惩罚似乎没有那么严厉,我们不会将线程永远卡在for循环中,这种情况可能非常危险并且很难诊断。
另外,总的来说,我希望避免这种循环,而应使用更具可读性的foreach样式循环。
我认为大多数程序员都使用第二个,因为它有助于弄清楚循环中发生了什么。我可以看一下,并且"知道"我将从0开始,并且肯定小于N。
第一变种没有这种品质。我可以看一下,我所知道的是我将从0开始,并且永远不会等于N。没有那么大的帮助。
无论我们如何终止循环,请务必谨慎使用循环外的循环控制变量。在示例中,我们(正确地)在循环内声明了i,因此它不在循环外的范围内,其值的问题尚无定论……
当然,第二个变体也有一个优点,那就是我看到的所有C引用都使用它:-)
我更喜欢使用#2,仅是因为我尝试不将i的含义扩展到for循环之外。如果我正在跟踪这样的变量,那么我将创建一个额外的测试。有人可能会说这是多余的或者效率低下的,但这使读者想起了我的意图:此时,我必须等于N
@timyates我同意一个人不应该依赖副作用
我倾向于使用第二种形式,只是因为这样我才能确定循环将终止。 IE。通过在循环内更改i来引入非终止错误比较困难。
当然,它还具有稍微懒惰的优点,即只需键入一个少字符即可;)
我还要指出,在具有合理范围规则的语言中,由于我在循环结构内部声明,因此不应在循环外部使用。这样可以减轻在循环结束时对i等于N的依赖...
我认为我们很好地说明了两者之间的区别。我确实有以下评论:
- 这不是"与语言无关",我可以看到示例在C ++中,但是有些语言不允许我们在循环内修改循环变量,而其他语言则不能保证索引值在使用之后就可用循环(有些同时做)。
- 我们已经在
for
中声明了i
索引,因此在循环之后我不会押注i
的值。这些示例有点误导,因为它们隐式地假设" for"是一个确定的循环。实际上,这只是一种更方便的书写方式:
// version 1 { int i = 0; while (i != N) { ... ++i; } }
注意在块之后如何未定义" i"。
如果程序员知道以上所有内容都不会对i的值做一般假设,并且明智地选择i <N作为结束条件,以确保最终满足退出条件。
如果我们在循环外使用i,则在C中使用以上任何一种都会导致编译器错误
有时我更喜欢这样:
for (int i = 0; (i <= (n-1)); i++) { ... }
这个版本直接显示了我可以拥有的值的范围。我检查范围的上下限的想法是,如果我们确实需要此范围,则代码会有太多的副作用,需要重写。
其他版本:
for (int i = 1; (i <= n); i++) { ... }
确定调用循环主体的频率。这也有有效的用例。
对于一般的编程工作,我更喜欢
for ( int i = 0; i < N; ++i )
到
for ( int i = 0; i != N; ++i )
因为它不太容易出错,尤其是在重构代码时。我已经看到这种代码无意间变成了无限循环。
该论点使得"我们将很容易假设i == N",我认为这是不对的。我从未做过这样的假设,也从未见过其他程序员做过。