ブレーク/リターンを使用した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をaしますb

それでも、最初のバージョンはそれほど明確ではないとは言えません。少なくとも初心者にとっては、さらに明確で理解しやすいかもしれません。だから私はまだどちらが良いのか自分自身に質問していますか?

たぶん誰かが方法の1つを支持して良い根拠を与えることができますか?

更新:これは、複数の関数の戻り点、ラムダ、または配列自体の要素の検出の問題ではありません。それは、単一の不等式よりも複雑な不変式を持つループを書く方法についてです。

更新:OK、答えてコメントする人々のポイントがわかります:ここでforeachループをミックスインしました。これは、whileループよりもはるかに明確で読みやすいものです。私はそうするべきではありませんでした。しかし、これも興味深い質問なので、そのままにしておきます。foreach-loopと内部の追加条件、または明示的なループ不変条件とpost-conditionの後のwhileループです。条件と終了/ブレークのあるforeachループが勝っているようです。foreach-loopを使用せずに追加の質問を作成します(リンクリストの場合)。


2
ここで引用したコード例には、いくつかの異なる問題が混在しています。アーリー&マルチプルリターン(私にとってはメソッドのサイズ(図示せず)になります)、配列検索(ラムダに関する議論を頼みます)、foreachと直接インデックス付け...この質問はより明確で簡単です一度にこれらの問題の1つだけに焦点を当てた場合に答えてください。
エリックエイド


1
私はそれらが例であることを知っていますが、そのユースケースを正確に処理するAPIを備えた言語があります。すなわちcollection.contains(foo)
ベリン・ロリッチ

2
あなたは本を見つけて、それを実際に読み直して実際に何を言ったかを見ることができます。
するThorbjörnRavnアンデルセン

1
「より良い」は非常に主観的な言葉です。とはいえ、最初のバージョンが何をしているのか一目でわかります。2番目のバージョンがまったく同じことを行うには、ある程度の精査が必要です。
デビッドハンメン

回答:


19

このような単純なループの場合、標準の最初の構文ははるかに明確だと思います。複数のリターンが混乱したりコードの匂いがすることを考える人もいますが、これほど小さなコードの場合、これが本当の問題だとは思いません。

より複雑なループについては、もう少し議論の余地があります。ループの内容が画面に収まらず、ループ内に複数のリターンがある場合、複数の出口点によりコードの保守が難しくなる可能性があるという議論があります。たとえば、関数を終了する前に何らかの状態維持メソッドが実行されていることを確認する必要がある場合、returnステートメントの1つにそれを追加し忘れてしまい、バグが発生する可能性があります。whileループですべての終了条件をチェックできる場合、出口ポイントは1つしかなく、このコードをその後に追加できます。

そうは言っても、特にループでは、できるだけ多くのロジックを別々のメソッドに入れてみるのが良いでしょう。これにより、2番目の方法に利点がある多くのケースが回避されます。明確に分離されたロジックを持つリーンループは、これらのスタイルのどちらを使用するかよりも重要です。また、アプリケーションのコードベースのほとんどが1つのスタイルを使用している場合、そのスタイルを使用する必要があります。


56

これは簡単です。

読者にとって明快さ以上に重要なことはほとんどありません。私が最初に発見したバリアントは、信じられないほどシンプルで明確です。

2番目の「改善された」バージョンでは、何度も読み、すべてのエッジ条件が正しいことを確認する必要がありました。

より良いコーディングスタイルであるZERO DOUBTがあります(最初の方がはるかに優れています)。

今-人々にとって明確であるものは、人によって異なる場合があります。そのための客観的な基準があるかどうかはわかりません(このようなフォーラムに投稿し、さまざまな人々の意見を得ることが役立つかもしれませんが)。

ただし、この特定のケースでは、最初のアルゴリズムがより明確である理由を説明できます。C++がコンテナ構文を反復処理し、どのように見えるかを知っています。私はそれを内面化しました。その構文を持つUNFAMILIAR(新しい構文)の誰かが2番目のバリエーションを好むかもしれません。

しかし、新しい構文を知って理解したら、その基本的な概念をそのまま使用できます。ループ反復(2番目)アプローチでは、ユーザーがすべてのエッジ条件を正確にチェックして配列全体をループしていることを慎重に確認する必要があります(たとえば、テストに使用される同じインデックスの代わりに、インデックス作成など)。


4
2011年の標準に既にあったように、新しいものは相対的です。また、2番目のデモは明らかにC ++ではありません。
デュプリケータ

あなたは、単一の出口点を使用したい場合は、代替ソリューションは、フラグを設定することですlongerLength = true、その後、return longerLength
カラブ

@Deduplicatorなぜ2番目のデモはC ++ではないのですか?なぜそうなのか分からない、または明らかな何かを見逃しているのか?
Rakete1111

2
@ Rakete1111 Raw配列には、などのプロパティはありませんlength。ポインタではなく配列として実際に宣言されている場合、を使用できますsizeof。またはである場合std::array、正しいメンバー関数はsize()であり、lengthプロパティはありません。
IllusiveBrian

@IllusiveBrian:sizeofバイト単位になります... C ++ 17が以来最も一般的ですstd::size()
デュプリケータ

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

[…]すべてがより明確になり、構造化プログラミングの方法でより明確になります

そうでもない。ここで変数iはwhileループの外側に存在するため、外側のスコープの一部ですxが、for-loopの(しゃれを意図した)ループのスコープ内にのみ存在します。スコープは、プログラミングに構造を導入する非常に重要な方法の1つです。



1
@ruakhあなたのコメントから何を奪うべきかわかりません。私の答えがwikiページに書かれていることに反対するかのように、それはやや受動的で攻撃的です。詳しく説明してください。
nullの

「構造化プログラミング」は特定の意味を持つ芸術用語であり、OPは客観的に正しいものであり、バージョン#2は構造化プログラミングのルールに準拠しますが、バージョン#1はそうではありません。あなたの答えから、あなたは芸術の用語に精通しておらず、その用語を文字通り解釈しているように見えました。私のコメントがなぜ消極的で積極的であるのかはわかりません。私は単に有益なものとしてそれを意味しました。
-ruakh

@ruakhバージョン2があらゆる面でより規則に準拠していることに同意せず、それを私の答えで説明しました。
nullの

あなたは、それが主観的なものであるかのように「同意しません」と言いますが、そうではありません。ループ内から戻ることは、構造化プログラミングの規則に対するカテゴリ違反です。多くの構造化プログラミング愛好家は最小スコープの変数のファンであると確信していますが、構造化プログラミングから逸脱して変数のスコープを縮小すると、構造化プログラミング、期間から逸脱し、変数のスコープを縮小しても元に戻りませんそれ。
ruakh

2

2つのループのセマンティクスは異なります。

  • 最初のループは、単純な「はい/いいえ」の質問に答えます。「配列には探しているオブジェクトが含まれていますか?」それは可能な限り最も簡単な方法で行います。

  • 2番目のループは、「配列に探しているオブジェクトが含まれている場合、最初の一致のインデックスは何ですか?」という質問に答えます。繰り返しますが、可能な限り最も短い方法でそうします。

2番目の質問に対する答えは、最初の質問に対する答えよりも厳密に多くの情報を提供するため、2番目の質問に答えてから、最初の質問の答えを導き出すことができます。return i < array.length;とにかく、それはラインがすることです。

既存のより柔軟なツールを再利用できない限り通常は目的に合ったツールを使用するのが最善だと思います。すなわち:

  • ループの最初のバリアントを使用しても問題ありません。
  • 最初のバリアントを変更してbool変数とブレークを設定するだけでも問題ありません。(2番目のreturnステートメントを避け、答えは関数の戻り値の代わりに変数で使用できます。)
  • 使用std::findは問題ありません(コードの再利用!)。
  • ただし、findを明示的にコーディングしてから、aへの回答を減らすことboolはできません。

downvotersがコメントを残す場合いいだろう...
cmaster-モニカを復元18年

2

3番目のオプションをすべて提案します。

return array.find(value);

配列を反復処理するさまざまな理由があります。特定の値が存在するかどうかを確認し、配列を別の配列に変換し、集計値を計算し、配列からいくつかの値をフィルタリングします。特にforループの使用方法が一目でわかります。ただし、現代のほとんどの言語では、配列データ構造に豊富な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));

3つのケースすべてで、forループは確かに読み取り可能ですが、forループの使用方法を把握し、すべてのカウンターと終了条件が正しいことを確認するために少し時間を費やす必要があります。モダンなラムダスタイルの関数は、ループの目的を非常に明確にし、呼び出されるAPI関数が正しく実装されていることを確実に知っています。

JavaScriptRubyC#、およびJavaを含むほとんどの最新の言語は、配列および類似のコレクションとのこのスタイルの機能的相互作用を使用します。

一般に、forループを使用することは必ずしも間違っているとは思わず、個人的な好みの問題ですが、このスタイルの配列を使用することを強く好みます。これは、具体的には、各ループが何をしているのかを判断する際の明確さが向上したためです。あなたの言語が標準ライブラリに同様の機能やツールを持っている場合、このスタイルを採用することも検討することをお勧めします!


2
推奨することarray.findは、実装するための最良の方法を議論する必要があるので、質問を頼みますarray.find。組み込みのfind操作でハードウェアを使用していない限り、そこにループを作成する必要があります。
バーマー

2
@Barmar同意しない。私の答えで示したように、頻繁に使用される言語の多くはfind、標準ライブラリのような機能を提供します。間違いなく、これらのライブラリはfindforループを使用して実装され、その類縁はforループを使用しますが、それは優れた関数の機能です:技術的な詳細を関数のコンシューマーから抽象化し、プログラマーがそれらの詳細について考える必要がないようにします。そのfindため、forループを使用して実装される可能性が高い場合でも、コードを読みやすくするのに役立ちます。また、標準ライブラリに頻繁にあるため、使用しても意味のあるオーバーヘッドやリスクはありません。
ケビン-復帰モニカ

4
ただし、ソフトウェアエンジニアはこれらのライブラリを実装する必要があります。ライブラリ作成者には、アプリケーションプログラマーと同じソフトウェアエンジニアリングの原則が当てはまりませんか?問題は、一般にループを記述することであり、特定の言語で配列要素を検索する最良の方法ではありません
-Barmar

4
別の言い方をすれば、配列要素の検索は、さまざまなループ手法を示すために使用した単純な例にすぎません。
バーマー

-2

それはすべて、「より良い」が意味するものに正確に要約されます。実用的なプログラマーにとっては、一般に効率的です。つまり、この場合、ループから直接抜けることで余分な比較を避け、ブール定数を返すことで重複した比較を避けます。これにより、サイクルが節約されます。ダイクストラは、正しいことを証明しやすいコードの作成に関心を持っています。[ヨーロッパのCS教育は、経済的力がコーディングの慣行を支配する傾向がある米国のCS教育よりも「コードの正確性の証明」をはるかに真剣に受け止めているように思われます]


3
PMar、パフォーマンスに関しては、両方のループはほぼ同等です。どちらも2つの比較があります。
ダニラピアトフ

1
パフォーマンスを本当に重視する場合は、より高速なアルゴリズムを使用します。たとえば、配列をソートしてバイナリ検索を実行するか、ハッシュテーブルを使用します。
user949300

ダニラ-この背後にあるデータ構造はわかりません。イテレータは常に高速です。インデックス付きアクセスは線形時間であり、長ささえ存在する必要はありません。
gnasher729
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.