成功するまで繰り返し、失敗を処理するループを構築する方法


8

私は独学のプログラマーです。私は約1.5年前にプログラミングを始めました。今、私は学校でプログラミングクラスを始めました。プログラミングクラスは1/2年間ありましたが、今はもう半分あります。

クラスでは、C ++でプログラミングする方法を学びます(これは、始める前に、私がすでにかなりよく使用することを知っていた言語です)。

このクラスでは何の問題もありませんでしたが、明確な解決策を見つけることができなかった問題が繰り返し発生しています。

問題は次のようになります(疑似コード):

 do something
 if something failed:
     handle the error
     try something (line 1) again
 else:
     we are done!

これはC ++の例です

このコードはユーザーに数値の入力を求め、入力が有効になるまで入力を続けます。cin.fail()入力が無効かどうかを確認するために使用します。ときcin.fail()であるtrue私が呼び出す必要がありますcin.clear()し、cin.ignore()ストリームからの入力を取得し続けることができるように。

このコードはをチェックしないことを知っていますEOF。私たちが書いたプログラムは、そうすることを期待されていません。

これが私が学校での課題の1つでコードを書いた方法です。

for (;;) {
    cout << ": ";
    cin >> input;
    if (cin.fail()) {
        cin.clear();
        cin.ignore(512, '\n');
        continue;
    }
    break;
}

私の先生は、私はこれを使うべきではないbreakcontinue言っていました。彼は代わりにレギュラーwhiledo ... whileループを使うべきだと提案しました。

このようなループを表す最も簡単な方法は、breakとを使用しているように思えcontinueます。私は実際にそれについてかなり長い間考えましたが、より明確な解決策を実際には思い付きませんでした。

彼は私に次のようなことをして欲しかったと思います:

do {
    cout << ": ";
    cin >> input;
    bool fail = cin.fail();
    if (fail) {
        cin.clear();
        cin.ignore(512, '\n');
    }
} while (fail);

私にfailは、追跡するために呼び出される変数もあり、入力失敗のチェックが1 回ではなく2回行われるため、このバージョンはかなり複雑に見えます。

また、次のようなコードを記述できることもわかりました(短絡評価を悪用しています)。

do {
    cout << ": ";
    cin >> input;
    if (fail) {
        cin.clear();
        cin.ignore(512, '\n');
    }
} while (cin.fail() && (cin.clear(), cin.ignore(512, '\n', true);

このバージョンは、他のバージョンとまったく同じように機能します。breakまたは使用せずcontinuecin.fail()テストは1回だけ実行されます。しかし、私がこのような「短絡評価ルール」を悪用することは、私には正しくないと思われます。私の先生もそれを望んでいないと思います。

この問題はcin.fail()チェックだけに当てはまるわけではありません。私はこれを、他の多くの場合に使用breakcontinue、条件が満たされるまでコードのセットを繰り返すことを含み、条件が満たされない場合は何かを実行する必要があります(呼び出しcin.clear()や例cin.ignore(...)からcin.fail())。

私はコースをずっと使用breakし続けcontinueましたが、今では私の先生はそれについて不平を言うのをやめました。

これについてのあなたの意見は何ですか?

私の先生は正しいと思いますか?

この種の問題を表すより良い方法を知っていますか?


6
あなたは永遠にループしているわけではないので、なぜあなたが永遠にループしていることを意味するものを使用しているのですか?ループはいつまでも悪い選択ではありません(通常はそうですが)。この場合、コードの間違った意図を伝えるため、ループは明らかに悪い選択です。2番目の方法はコードの目的に近いため、より優れています。2番目のスニペットのコードは、いくつかの最小限の再編成で、少し整然と読みやすくすることができます。
2015年

3
@ダンクそれは少し独断的なようです。インタラクティブなプログラムのトップレベル以外では、永久にループする必要があることは実際には非常にまれです。私が見るwhile (true)とすぐにありますと仮定しbreakreturnまたはthrowそこにどこかで。ちょうどかわすのトラックに保持する必要があり、余分な変数の紹介breakcontinue逆効果であるが。最初のコードの問題は、ループが正常に反復を終了しないことと、continue内部にあるifことを知って、breakウィークが取得されないことを知る必要があることです。
Doval

1
例外処理を学ぶには早すぎますか?
モーグはモニカを2015

2
@ダンク何を意図していたかすぐにわかるかもしれませんが、一時停止して、それが正しいことを確認するのが難しいと考える必要あります。変更可能なので、ループが正しく終了し、私がチェックする必要があることを知っていることのセットデータが有効である場合に、それはでは未設定のないことを任意のその後のポイント。次の形式のループでは、がに評価された場合、ループ本体の残りの部分を確認しなくてもループ中断することわかっています。dataIsValiddo { ... if (!expr) { break; } ... } while (true);exprfalse
ドバル2015年

1
@Doval:私はコードの読み方もわかりませんが、コードが何をしているか、どこにエラーがあるのか​​を理解しようとしているときに、すべての行を読むことはできません。whileループを見ると、ループが有効なデータを取得していることが(私の例から)すぐにわかります。私はそれがどのようにそれをしているのか気にしません、何らかの理由で無効なデータを取得していることが起こらない限り、ループ内の他のすべてをスキップできます。次に、ループ内のコードを確認します。あなたのアイデアでは、ループ内のすべてのコードを調べて、それが私にとって興味がないことを理解する必要があります。
2015年

回答:


15

私はifステートメントを少し異なるように書くので、入力が成功したときにそれが取られます。

for (;;) {
    cout << ": ";
    if (cin >> input)
        break;
    cin.clear();
    cin.ignore(512, '\n');
}

それも短いです。

先生が好むかもしれない短い方法を示唆しています:

cout << ": ";
while (!(cin >> input)) {
    cin.clear();
    cin.ignore(512, '\n');
    cout << ": ";
}

そのように条件文を逆にするのでbreak、最後にaは必要ありません。それだけでも、コードが読みやすくなります。それ!(cin >> input)が有効であることも知りませんでした。知らせてくれてありがとうございます!
wefwefa3 2015年

8
2番目のソリューションの問題は、行を複製するcout << ": ";ため、DRYの原則に違反することです。実際のコードの場合、エラーが発生しやすくなるため、これは実行できません(後でその部分を変更する必要がある場合は、同じ方法で2行を変更することを忘れないようにする必要があります)。Neverthelss私はあなたにあなたの最初のバージョンの+1を与えました、それは私見をbreak適切に使用する方法の例です。
Doc Brown

2
複製されたラインが明らかに少し控えめなUIであることを考えると、これは髪の毛をピッキングしているように感じます。あなたは最初のものをcout << ": "完全に取り除くことができ、何も壊れません。
jdevlin

2
@codingthewheel:あなたは正しいです。この例は、DRY原則を無視することで起こり得るリスクを示すには簡単すぎます。しかし、実際のコードでこのような状況を想像してみてください。偶然にバグを導入すると、実際に結果が生じる可能性があります。その場合、別の考えをするかもしれません。そのため、30年間のプログラミングの後、最初の部分を「whileコマンドがより見栄えが良いため」に複製することで、このようなループ終了の問題を解決することはありませんでした
Doc Brown

1
@DocBrown coutの複製は、ほとんどの例の成果物です。2番目のステートメントにはエラーメッセージが含まれるため、実際には2つのcoutステートメントは異なります。たとえば、「<foo>を入力してください:」「エラー!<foo>を再入力してください:」などです。
Sjoerd、2015年

15

あなたが努力しなければならないのは、生のループを避けることです。

複雑なロジックをヘルパー関数に移動すると、突然物事が非常に明確になります。

bool getValidUserInput(string & input)
{
    cout << ": ";
    cin >> input;
    if (cin.fail()) {
        cin.clear();
        cin.ignore(512, '\n');
        return false;
    }
    return true;
}

int main() {
    string input;
    while (!getValidUserInput(input)) { 
        // We wait for a valid user input...
    }
}

1
これは私が提案しようとしていたことのほとんどです。入力を取得する決定と入力を取得することを区別するようにリファクタリングします。
Rob K

4
私はこれが素晴らしい解決策であることに同意します。getValidUserInput一度だけではなく、何度も呼び出されると、はるかに優れた選択肢になります。私はあなたに+1を与えますが、代わりにSjoerdの答えを受け入れます。なぜなら、それが私の質問によりよく答えるからです。
wefwefa3 2015年

@ user3787875:私が2つの答えを読んだときに私が考えたこととまったく同じ-良い選択。
Doc Brown

これは私の答えの書き換え後の論理的な次のステップです。
Sjoerd、2015年

9

for(;;)悪いのはそれほどではありません。次のようなパターンほど明確ではありません。

while (cin.fail()) {
    ...
}

またはSjoerdが言ったように:

while (!(cin >> input)) {
    ...
}

このようなものの主な対象者は、あなたが仲間のプログラマーであると考えてみましょう。そのようにして、最後にブレークをスタックしたか、継続してブレークをジャンプした理由を覚えていない自分の将来のバージョンを含みます。あなたの例からのこのパターン:

for (;;) {
    ...
    if (blah) {
        continue;
    }
    break;
}

...他のパターンと比較してシミュレーションするには、数秒の追加の思考が必要です。これは難しい規則ではありませんが、continueを使用してbreakステートメントをジャンプするのは面倒で、役に立たないので賢く、ループの最後にbreakステートメントを配置することは、たとえそれが機能したとしても、異常です。通常は両方breakcontinueするために使用されている途中でその最後のいずれかで彼らの見て、ループまたはループの反復を避難そうでない場合でも、少し奇妙な感じ。

それ以外に、良いもの!


あなたの答えは素晴らしく見えますが、一見しただけです。実際、これは@Sjoerdのs answer: using a while`ループで見ることができる固有の問題を隠しているため、このように指定された例では、不要なコードの重複につながる可能性があります。そしてそれは、ループの全体的な可読性と同じくらい少なくとも対処することが重要な問題です。
Doc Brown

リラックスして、ドク。OPは彼のループ構造に関するいくつかの基本的なアドバイス/フィードバックを求め、私は彼に標準的な解釈を与えました。あなたは他の解釈について議論することができます-標準のアプローチが常に最良または最も数学的に厳密なアプローチであるとは限りません-しかし、特に彼自身のインストラクターはの使用に傾倒していたのでwhile、私は「一見だけ」ビットは正当化されないと思います。
jdevlin

誤解しないでください。これは「数学的に厳密」ではありません。これは、進化するコードを書くことに関するものです。新しい機能を追加したり、既存の機能を変更したりすると、バグが発生するリスクが最小限になります。私の仕事では、それは本当に学問的な問題ではありません。
Doc Brown

私はあなたの答えにほとんど同意することを付け加えたいと思います。最初に簡略化すると、頻繁に発生する問題隠れてしまう可能性があることを指摘しておきたいと思います。コードの重複なしに「while」を使用する機会があれば、遠慮なく使用します。
Doc Brown

1
同意し、時にはループの前の不要な行が、宣言的なフロントローディングwhile構文に支払う代償です。ループの内側で行われるループの前に何かをするのは好きではありません。また、接頭辞をリファクタリングしようとしてノットに結びつくループも好きではありません。したがって、少しバランスをとるのはどちらの方法でも可能です。
jdevlin

3

学校のロジックインストラクターはいつも、ループに入る入り口と出口は1つだけであるべきだと言って、それを私の脳に押し込みました。そうでない場合は、スパゲッティコードの取得を開始し、デバッグ中にコードがどこに行くのかを知りません。

現実には、コードの読みやすさは非常に重要であると感じています。そのため、他の誰かがコードの問題を修正またはデバッグする必要がある場合でも、簡単に実行できます。

また、この外観を何百万回も実行しているためパフォーマンスの問題である場合は、forループの方が高速かもしれません。テストはその質問に答えます。

C ++は知りませんが、コード例の最初の2つは完全に理解しました。continueがforループの終わりに行き、それを再びループすると仮定しています。whileの例は完全に理解しましたが、for(;;)の例よりも理解しやすかったです。3つ目は、それを理解するためにいくつかの思考作業を行わなければならないでしょう。

したがって、読みやすく(c ++を知らない)、私のロジックインストラクターがwhileループ1を使用すると言っていたのは、私のためです。


4
それで、あなたのインストラクターは例外の使用に反対ですか?そして早期復帰?そして同様のもの?適切に使用すると、すべてのコードが読みやすくなります...
デデュプリケーター

1
省略された文のキーフレーズは「適切に採用されている」ことです。私の苦い経験は、このクレイジーなラケットのNOBODYの近くでのろわれた人が、「適切に」のほとんどすべての合理的な定義のために、それらを「適切に」使用する方法を知っていることを示しているように思えます。
John R. Strohm

2
生徒が初心者であるクラスについて話していることを思い出してください。確かに、彼らが初心者の間に一般的な問題を引き起こすこれらのことを回避するように彼らを訓練することは(彼らが意味を理解し、ルールの例外となるものについて推論する前に)良い考えです。
user1118321 2015年

2
「1つの入り口-一度の出口」という定説は、時代遅れの視点である私見であり、Basic、Pascal、COBOLなどの言語でGOTOをいじくり回しているときに、おそらくEdward Dijkstraによって「発明」されました。私は、Deduplicatorが上に書いたものに完全に同意します。さらに、OPの最初の2つの例には、実際には1つのエントリと1つの出口しか含まれていませんが、読みやすさは平凡です。
Doc Brown

4
@DocBrownに同意します。現代のプロダクションコードベースでの「1つの入り口と1つの出口」は、コードの乱れのレシピになる傾向があり、制御構造の深いネストを促進します。
jdevlin

-2

私にとって、疑似コードの最も簡単なC変換は

do
    {
    success = something();
    if (success == FALSE)
        {
        handle_the_error();
        }
    } while (success == FALSE)
\\ moving on ...

この明白な翻訳が問題である理由がわかりません。

おそらくこれ:

while (!something())
    {
    handle_the_error();
    }

シンプルに見えます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.