带中断/返回的Foreach循环与带显式不变和后置条件的while循环


17

这是检查值是否在数组中的最流行的方法(在我看来):

for (int x : array)
{
    if (x == value)
        return true;
}
return false;        

但是,在我也许是Wirth或Dijkstra读过的一本书中,据说这种风格更好(与内部退出的while循环相比):

int i = 0;
while (i < array.length && array[i] != value)
    i++;
return i < array.length;

这样,附加的退出条件就成为循环不变式的显式部分,没有隐藏的条件,并且在循环内退出,一切都变得更加明显,并且结构化编程的方式更加明显。我通常优选后者的图案尽可能和所使用的for从-loop只迭代ab

但是我不能说第一个版本不太清楚。至少对于初学者来说,它甚至更清晰,更容易理解。所以我仍然在问自己一个问题哪个更好?

也许有人可以对其中一种方法给出很好的理由?

更新:这不是多个函数返回点,lambda或本身在数组中查找元素的问题。这是关于如何编写具有比单个不等式更复杂的不变式的循环。

更新:好的,我看到回答和评论的人的观点:我在这里混入了foreach循环,它本身已经比while循环更加清晰易读。我不应该那样做。但这也是一个有趣的问题,因此我们将其保留为:foreach循环和内部一个额外条件,或者while循环具有一个显式循环不变性和after后置条件。看来带有条件和退出/中断的foreach循环是成功的。我将创建一个没有foreach循环的附加问题(用于链接列表)。


2
这里引用的代码示例混合了几个不同的问题。早期和多次返回(对我来说,要达到方法的大小(未显示)),数组搜索(对涉及lambda的讨论感到困惑),foreach与直接索引...这个问题将更清晰,更容易如果它一次只关注这些问题之一,则回答。
埃里克·埃德特


1
我知道这些是示例,但是有些语言具有API来准确处理该用例。即collection.contains(foo)
帐户berin Loritsch

2
您可能想要找到这本书并立即重读以查看其实际含义。
托尔比约恩Ravn的安德森

1
“更好”是一个非常主观的词。也就是说,您可以一目了然地看出第一个版本的功能。第二个版本做完全相同的事情需要仔细审查。
大卫·哈门

Answers:


19

我认为对于诸如此类的简单循环,标准优先语法要清晰得多。有人认为多次返回会造成混乱或代码异味,但是对于一段如此小的代码,我认为这不是一个真正的问题。

对于更复杂的循环,它更具争议性。如果循环的内容无法显示在屏幕上,并且循环中有多个返回值,则存在一个论点,即多个退出点会使代码更难以维护。例如,如果您必须确保在退出函数之前运行了某种状态维护方法,那么很容易会错过将其添加到return语句之一的麻烦,并且会导致错误。如果可以在while循环中检查所有结束条件,则您只有一个退出点,可以在其后添加此代码。

也就是说,特别是对于循环,最好尝试将尽可能多的逻辑放入单独的方法中。这避免了第二种方法具有优势的许多情况。逻辑清晰的精益循环比使用哪种样式更重要。另外,如果应用程序的大多数代码库都使用一种样式,则应坚持使用该样式。


56

这很容易。

对读者来说,几乎没有什么比澄清。我发现第一个变种非常简单明了。

第二个“改进”版本,我必须阅读几次,并确保所有边缘条件都正确。

ZERO DOUBT是更好的编码样式(第一个更好)。

现在-对人们的清除可能因人而异。我不确定是否有任何客观标准(尽管在这样的论坛上发帖并获得各种人的帮助会有所帮助)。

但是,在这种特殊情况下,我可以告诉您为什么第一个算法更清晰:我知道C ++遍历容器语法的方式和作用。我已经内化了。使用该语法的人(其新语法)可能更喜欢第二种变体。

但是一旦您知道并理解了这种新语法,就可以使用它的基本概念。使用循环迭代(第二种)方法时,您必须仔细检查用户是否正确检查了要在整个阵列上循环的所有边缘条件(例如,小于用于测试和测试的相同或更少的相同索引)。用于索引等)。


4
新功能是相对的,因为它已经存在于2011年标准中。另外,第二个演示显然不是C ++。
Deduplicator

如果要使用单个出口点,另一种解决方案是设置一个标志longerLength = true,然后return longerLength
Cullub

@Deduplicator为什么第二个演示C ++没有?我不明白为什么不这样做,或者我缺少明显的东西吗?
Rakete1111 '18

2
@ Rakete1111原始数组没有类似的任何属性length。如果实际上将其声明为数组而不是指针,则可以使用sizeof,或者如果将其声明为std::array,则正确的成员函数为size(),则没有length属性。
IllusiveBrian

@IllusiveBrian:sizeof将以字节为单位...由于C ++ 17是最通用的std::size()
Deduplicator

9
int i = 0;
while (i < array.length && array[i] != value)
    i++;
return i < array.length;

[…]一切都更加明显,并且更多地以结构化编程的方式进行。

不完全的。可变i存在这里while循环外,因此是外范围的一部分,而(双关语意)xfor-loop只在循环的范围内是否存在。范围是将结构引入编程的一种非常重要的方法。



1
@ruakh我不确定您要说些什么。它看起来有点消极进取,好像我的回答与Wiki页面上写的相反。请详细说明。
null

“结构化编程”是具有特定含义的技术术语,并且OP客观上正确地认为版本#2符合结构化编程的规则,而版本#1则不符合。从您的回答看来,您似乎并不熟悉艺术术语,而是从字面上解释该术语。我不确定为什么我的评论会被认为是被动攻击性的。我的意思仅仅是提供信息。
ruakh

@ruakh我不同意版本2在各个方面都更符合规则,并在回答中对此进行了解释。

您说“我不同意”,就好像这是一个主观的事情,但不是。从循环内部返回是对结构化编程规则的绝对违反。我敢肯定,许多结构化编程爱好者都喜欢最小范围的变量,但是如果您通过偏离结构化编程来缩小变量的范围,那么您就背离了结构化编程,句号,并且减小了变量的范围并不会撤消那。
ruakh

2

这两个循环具有不同的语义:

  • 第一个循环仅回答一个简单的是/否问题:“数组是否包含我要查找的对象?” 它以最简短的方式这样做。

  • 第二个循环回答以下问题:“如果数组包含我要查找的对象,则第一个匹配项的索引是什么?” 同样,它以最简短的方式这样做。

由于第二个问题的答案确实比第一个问题的答案提供了更多的信息,因此您可以选择回答第二个问题,然后得出第一个问题的答案。return i < array.length;无论如何,这就是生产线所要做的。

我认为通常最好只使用适合目的的工具,除非您可以重用已经存在的,更灵活的工具。即:

  • 使用循环的第一个变体就可以了。
  • 将第一个bool变量更改为仅设置变量并中断也可以。(避免使用第二条return语句,答案可以在变量中使用,而不能在函数返回中使用。)
  • 使用std::find很好(代码重用!)。
  • 但是,显式编码查找结果然后将答案简化为a bool并非如此。

如果
下降投票者

2

我将建议第三种选择:

return array.find(value);

遍历数组有多种不同的原因:检查是否存在特定值,将数组转换为另一个数组,计算聚合值,从数组中筛选出一些值...如果使用普通的for循环,则尚不清楚一目了然,特别是如何使用for循环。但是,大多数现代语言在其数组数据结构上都有丰富的API,这些API使这些不同的意图非常明确。

比较一下使用for循环将一个数组转换为另一个数组:

int[] doubledArray = new int[array.length];
for (int i = 0; i < array.length; i++) {
  doubledArray[i] = array[i] * 2;
}

并使用JavaScript样式的map函数:

array.map((value) => value * 2);

或求和一个数组:

int sum = 0;
for (int i = 0; i < array.length; i++) {
  sum += array[i];
}

与:

array.reduce(
  (sum, nextValue) => sum + nextValue,
  0
);

您需要多长时间才能了解其作用?

int[] newArray = new int[array.length];
int numValuesAdded = 0;

for (int i = 0; i < array.length; i++) {
  if (array[i] >= 0) {
    newArray[numValuesAdded] = array[i];
    numValuesAdded++;
  }
}

array.filter((value) => (value >= 0));

在这三种情况下,虽然可以肯定地知道for循环,但是您必须花一些时间弄清楚for循环的使用方式,并检查所有计数器和退出条件是否正确。现代的lambda样式函数使循环的目的变得非常明确,并且可以肯定地知道所调用的API函数是正确实现的。

包括JavaScriptRubyC#Java在内的大多数现代语言都使用这种与数组和类似集合进行功能交互的样式。

总的来说,尽管我认为使用for循环不一定是错误的,这不是个人喜好,但我发现自己非常喜欢使用这种处理数组的方式。这特别是因为确定每个循环正在执行的操作的清晰度更高。如果您的语言在其标准库中具有类似的功能或工具,建议您也考虑采用这种样式!


2
推荐array.find提出了问题,因为我们接下来必须讨论实现的最佳方法array.find。除非您使用具有内置find操作的硬件,否则我们必须在其中编写一个循环。
Barmar

2
@Barmar我不同意。正如我在回答中指出的那样,许多常用语言都提供了类似find其标准库中的功能。无疑,这些库实现find了for循环,并使用for循环,但这就是一个好函数的作用:它将技术细节从函数的使用者中分离出来,从而使程序员无需考虑这些细节。因此,即使find可能通过for循环实现,它仍然有助于使代码更具可读性,并且由于它经常出现在标准库中,因此使用它不会增加任何有意义的开销或风险。
凯文(Kevin)

4
但是软件工程师必须实现这些库。图书馆作者和应用程序程序员难道没有相同的软件工程原理吗?问题是关于编写循环的一般问题,而不是搜索特定语言中的数组元素的最佳方法
Barmar,

4
换句话说,搜索数组元素只是他用来演示不同循环技术的简单示例。
Barmar

-2

一切都归结为“更好”的含义。对于实际的程序员来说,这通常意味着有效-即在这种情况下,直接从循环中退出可以避免一个额外的比较,而返回布尔常量则可以避免重复的比较。这样可以节省周期。Dijkstra更关心的是编写更容易证明正确的代码。[在我看来,欧洲的CS教育要比美国的CS教育更加重视“证明代码正确性”,而在美国,CS的经济力量往往占据主导地位。


3
PMar,就性能而言,这两个循环几乎是等效的-它们都有两个比较。
丹妮拉·皮亚托夫

1
如果真正关心性能,则可以使用更快的算法。例如,对数组进行排序并执行二进制搜索,或使用哈希表。
user949300

Danila-您不知道背后的数据结构是什么。迭代器总是非常快。索引访问可以是线性时间,甚至不需要长度。
gnasher729
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.