我应该期望在`for`循环中看到计数器在其主体内被更改吗?

16

我正在阅读别人的代码,他们在循环内部单独增加了for循环计数器,并且包含了通常的后续操作。例如:

for( int y = 4; y < 12; y++ ) {
    // blah
    if( var < othervar ) {
        y++;
    }
    // blah
}

根据大多数人编写和阅读的代码,我应该期望看到这个吗?


23
我认为这取决于具体情况。 - AndyG
3
显然不行:a)它会使代码更难阅读和理解。b)它可能导致一般情况下的无限循环。c)在修复代码的这些部分时,必须记住不要使其变成无限循环。 - Arkady
3
它将如何导致永久循环?(注意:条件是“y < 12”,而不是“y!= 12”。如果使用了“!=”,我会理解你的评论。) - user743382
2
对我来说,这是一种代码异味,总是如此。即使它在功能上很出色,可读性也很糟糕。 - Bradley Thomas
4
很抱歉我要成为一个持否定态度的人,但我仍然不明白这个问题为何不基于观点。也许在[programmers.se]上更适合?(我已经尽力使翻译通俗易懂,但未改变原意,并且没有提供解释或其他额外内容。) - Sourav Ghosh
显示剩余7条评论
7个回答

32
在for循环中操作循环计数器的做法并不普遍。这会让很多阅读该代码的人感到惊讶。而且“惊讶”读者通常不是一个好主意。对循环计数器进行额外的操作会给你的代码添加大量复杂性,因为你必须记住它的含义以及它如何影响循环的整体行为。正如Arkady所提到的,这使得你的代码更难维护。简而言之,请避免使用这种模式。当你遵循“清洁代码”原则时,特别是单一抽象层 (SLA)原则时,不存在这样的问题。
for(something)
  if (somethingElse)
   y++

遵循这个原则需要您将那个 if 块移入自己的方法中,这会使得在该方法中操作一些外部计数器变得笨拙。

但除此之外,可能会出现类似您示例的情况;但对于这些情况,为什么不使用 while 循环呢?

换句话说:使您的示例复杂和混乱的是代码中的两个不同部分更改了您的循环计数器。因此,另一种方法可能如下:

 while (y < whatever) {
   ...
   y = determineY(y, who, knows);
 }

那种新方法可以成为一个核心地方,来确定如何更新循环变量。


7
除了这种做法非常普遍外,没有其他需要说明的内容。 - Oktalist
5
“惊讶读者从来不是一个好主意。”这是一种观点。虽然它非常普遍,但这只是一种观点。按照清晰代码原则行事始终是个好主意,也是一个观点而已。 - Mad Physicist
4
有时候,如果满足某个条件,你可能会想要跳过一个迭代。在循环中添加y++可以实现这一点。当然,这取决于上下文并且可能需要有明确的注释来解释正在发生的事情,但是我没有看到更好的替代方案,因为它们都需要明确的注释... - Bakuriu
5
您好,以下是翻译的结果:@mad,您能否举一个编写令读者感到惊讶的代码会更可取的例子呢?否则,您的评论似乎只是过度的学究,认为普遍持有的意见和非常有用的指南只是“意见”。实际上,几乎所有的东西都是一种观点;这个观点来自GhostCat,并且与他的名字相联系。 - Cody Gray
2
@CodyGray,问题很快变得哲学化。你的读者是谁,以及什么才能让他们感到惊讶?例如,根据Google开发人员的指南(至少我曾经看过的),使用C++中的模板是有争议的,如果你发现自己使用了模板模板参数,那么你正在做错事情。我不喜欢在软件开发中降低到最低公共分母的想法。 - SergeyA
显示剩余6条评论

27

我不同意上面所赞同的答案。在循环体内部操纵循环控制变量是没有问题的。例如,下面是清理地图的经典示例:

for (auto it = map.begin(), e = map.end(); it != e; ) {
    if (it->second == 10)
        it = map.erase(it);
    else
        ++it;
}

由于有人正确地指出迭代器并不等同于数值控制变量,因此让我们考虑一下解析字符串的示例。假设该字符串由一系列字符组成,其中以 '\' 为前缀的字符被视为特殊字符,需要跳过:

for (size_t i = 0; i < s_len; ++i) {
    if (s[i] == '\\') {
       ++i;
       continue;
    }
    process_symbol(s[i]);
}

1
我不同意原始示例是在两个地方操作数字循环计数器... 在我看来,这与您的代码完全不同,因为您的示例仅在原地更改其值,而不是两个! - GhostCat
2
@RobK 嗯,我认为问题很明显,在循环中的意图不是执行"k次"动作,否则为什么要以 y=4 开始呢?为了搞混读者吗?循环的目的是迭代一系列(大多数情况下)连续的数字,并为每个值执行某些操作。有时您需要跳过一个值(添加 y++ 将完美地解决这个问题)。 - Bakuriu
1
@SiyuanRen,请看更新的答案,我已经包含了那个。 - SergeyA
2
@Chimera 这是不可能的。即使 $ 是一个特殊符号,\$ 也不是特殊的。 process_symbol 每次只会给出一个字符,因此无法看到 $ 是由 \ 预先处理的。 - user743382
2
@LS97,it 的可见性可能是一个原因。 - SergeyA
显示剩余8条评论

10

使用 while 循环替代。

虽然你可以用 for 循环完成这个任务,但你不应该这样做。记住,程序就像其他任何沟通的工具一样,必须考虑到受众。对于程序,受众包括编译器和下一个要维护代码的人(可能是六个月后的你)。

对于编译器而言,代码被非常字面地执行——设置索引变量、运行循环体、执行增量,然后检查条件以查看是否需要再次循环。编译器并不关心你是否疯狂更改循环索引。

然而,对于人来说,for 循环有一个特定的含义:固定次数地运行此循环。如果你疯狂更改循环索引,那么这就违反了这种含义。从某种意义上说,这是不诚实的,也很重要,因为下一个阅读代码的人要么必须花费额外的精力来理解循环,要么将无法理解。

如果你想疯狂更改循环索引,请使用 while 循环。特别是在 C/C++/相关语言中,for 循环和 while 循环的功能完全相同,因此你不会失去任何功能或表达能力。任何 for 循环都可以转换为 while 循环,反之亦然。但是,下一个阅读代码的人不会依赖于你不疯狂更改循环索引的含义。将其变成 while 循环而不是 for 循环是一个警告,表示这种循环可能更加复杂,在你的情况下,确实更加复杂。


7
如果你在循环内递增,请确保加上注释。一个经典的例子(基于Scott Meyers的Effective C++条款)在Q&A中给出,链接为How to remove from a map while iterating it? (代码直接复制)。
for (auto it = m.cbegin(); it != m.cend() /* not hoisted */; /* no increment */)
{
  if (must_delete)
  {
    m.erase(it++);    // or "it = m.erase(it)" since C++11
  }
  else
  {
    ++it;
  }
}

在这里,end()迭代器的非常数性质以及循环内的增量都是令人惊讶的,因此需要进行记录。注意:此处循环提升最终可能应该为了代码清晰度而完成。


2
聪明人想法相似 :) 但是 end 迭代器没有失效,所以你不需要在每次迭代时调用它。 - SergeyA

3

值得一提的是,以下是C++核心准则对此主题的建议:

http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Res-loop-counter

ES.86: 避免在原生for循环的循环体内修改循环控制变量

原因:循环控制应该在前面,以使我们正确地推断出循环内发生了什么。在迭代表达式和循环体内修改循环计数器是引起意外和错误的固有方法。

同时,还要注意其他回答中讨论使用std::map的情况。在这种情况下,控制变量的增量仍然只会在每次迭代中执行一次,而在您的示例中,它可以在每次迭代中执行多次。


3
经过一些混淆,即关闭、重新打开、问题正文更新、标题更新等,我认为问题最终已经清晰明了。而且它也不再是基于个人意见的。
据我所知,这个问题是:
当我查看别人编写的代码时,我应该期望在循环体中更改"循环条件变量"吗?
答案很明确:
是的
当你与他人的代码共事-无论是审查、修复漏洞还是添加新功能-你都应该预料到最坏的情况。
任何语言中有效的内容都应该被预料到。
不要对代码符合任何良好实践的假设。

干得好!期望代码行为良好是不明智的。 - Keith

2

最好使用while循环来编写

y = 4;
while(y < 12)
{
   /* body */
   if(condition)
     y++;
   y++;
}

有时您可以将循环逻辑与主体分开。
 while(y < 12)
 {
    /* body */
    y += condition ? 2 : 1;
 }

如果您很少跳过项目,比如在带引号的字符串中使用转义符号,我会允许使用for()方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接