上司は、悪いプログラマーがbreak
とcontinue
をループで使用することをさりげなく言及し続けます。
彼らは意味をなすので、私はいつもそれらを使用しています。インスピレーションをお見せしましょう:
function verify(object) {
if (object->value < 0) return false;
if (object->value > object->max_value) return false;
if (object->name == "") return false;
...
}
ここでのポイントは、最初に関数が条件が正しいことを確認し、次に実際の機能を実行することです。 IMOはループにも適用されます。
while (primary_condition) {
if (loop_count > 1000) break;
if (time_exect > 3600) break;
if (this->data == "undefined") continue;
if (this->skip == true) continue;
...
}
これにより、読み取りとデバッグが容易になると思います。しかし、私はマイナス面も見ていません。
ブロックの最初に使用すると、最初のチェックが行われたとき、それらは前提条件のように機能するので良いです。
ブロックの中央で使用すると、コードがいくつかあるため、隠されたトラップのように動作するため、問題です。
Donald Knuthの1974年の論文 go to Statementsを使用した構造化プログラミング を読むと、構造的に望ましいgo to
のさまざまな使用法について説明されています。これらには、break
およびcontinue
ステートメントに相当するものが含まれます(そこでのgo to
の使用法の多くは、より制限された構造に発展しています)。あなたの上司はKnuthを悪いプログラマーと呼ぶタイプですか?
(私が興味を示した例。通常、break
とcontinue
は、任意のコードの1つのエントリと1つの出口が好きな人には嫌われており、そのような人も複数のreturn
ステートメント。)
私は彼らが悪いとは思わない。それらが悪いという考えは、構造化プログラミングの時代から来ています。これは、関数には単一の入口点と単一の出口点が必要であるという概念に関連しています。 e。関数ごとにreturn
は1つだけです。
これは、関数が長い場合や、ネストされたループが複数ある場合に意味があります。ただし、関数は短くする必要があり、ループとその本体を独自の短い関数にラップする必要があります。一般に、関数に単一の出口点を強制すると、非常に複雑なロジックになる可能性があります。
関数が非常に短い場合、ループが1つある場合、またはネストされたループが2つ以上ある場合、およびループ本体が非常に短い場合、break
またはcontinue
の機能は非常に明確です。また、複数のreturn
ステートメントの機能も明確です。
これらの問題は、Robert C. Martinによる「クリーンコード」およびMartin Fowlerによる「リファクタリング」で対処されています。
悪いプログラマーは絶対的に(シスのように)話します。優れたプログラマーは、可能な限り最も明確なソリューションを使用します(他のすべてのものは同じ)。
Break and continueを頻繁に使用すると、コードの追跡が難しくなります。しかし、それらを置き換えることでコードの追跡がさらに難しくなる場合、それは悪い変更です。
あなたが与えた例は、間違いなく中断と継続がよりエレガントなものに置き換えられるべき状況です。
ほとんどの人は、その振る舞いを簡単に予測できないので、それを悪い考えだと思っています。コードを読んでいてwhile(x < 1000){}
が表示される場合は、x> = 1000まで実行されると想定します...しかし、途中にブレークがある場合、それは成り立たないため、あなたは本当にあなたのループを信頼することはできません...
それは人々がGOTOを好まないのと同じ理由です。確かに、それはcanをうまく使用できますが、コードがセクションからセクションへランダムに跳躍するゴーダフルなスパゲッティコードにつながる可能性もあります。
私自身、複数の条件で壊れるループを実行する場合は、while(x){}
を実行し、ブレークアウトする必要があるときにXをfalseに切り替えます。最終的な結果は同じであり、コードを読んでいる人は誰でも、Xの値を切り替えたものをより詳しく調べることを知っています。
はい、breakステートメント(または同じことを行うループの途中から戻る)なしでプログラムを[再]作成できます。しかし、追加の変数やコードの重複を導入しなければならない場合があります。これらは両方とも、通常、プログラムを理解しにくくします。そのため、Pascal(プログラミング言語)は、特に初心者プログラマにとって非常に悪かった。上司は基本的に、Pascalの制御構造でプログラミングすることを望んでいます。 Linus Torvaldsがあなたの靴の中にいたら、彼はおそらくあなたの上司に中指を見せます!
コサラジュの制御構造の階層と呼ばれるコンピューターサイエンスの結果があります。これは、1973年にさかのぼり、1974年のゴトスに関するKnuthの(もっと)有名な論文で言及されています。 。)S. Rao Kosarajuが1973年に証明したことは、深さのマルチレベルのブレークnを持つすべてのプログラムを、ブレーク付きのプログラムに書き直すことができないということです。 n未満の深さで、追加の変数を導入しません。しかし、それは単なる理論上の結果だとしましょう。 (ちょうどいくつかの追加の変数を追加しますか?!きっとあなたはそれをあなたの上司を満足させるために行うことができます...)
ソフトウェアエンジニアリングの観点からはるかに重要なのは、Eric S. Robertsによる1995年の最近の論文Loop Exits and Structured Programming:Reopening the Debate( http://cs.stanford.edu/people/eroberts/papers/SIGCSE-1995/LoopExits.pdf )。ロバーツは彼の前に他の人が行ったいくつかの実証研究を要約しています。たとえば、CS101タイプの学生のグループが配列内の逐次検索を実装する関数のコードを書くように求められたとき、研究の著者は、ブレーク/リターン/ゴートを使用して終了する学生について次のように述べています要素が見つかったときの順次検索ループ:
私はまだ、[このスタイル]を使用してプログラムを試み、正しくない解決策を生み出した一人を見つけていません。
ロバーツはまたそれを言う:
Forループからの明示的な戻りを使用せずに問題を解決しようとした学生は、はるかにうまくいかなかった:この戦略を試みた42人の学生のうち、正しい解決策を生成できたのはわずか7人だけでした。その数字は、20%未満の成功率を表しています。
はい、CS101の学生よりも経験が豊富かもしれませんが、breakステートメント(または同等のループの途中からreturn/goto)を使用せずに、最終的には、名目上は適切に構造化されている一方で、追加のロジックの観点から十分に扱いにくいコードを記述します変数とコードの重複。誰かが、おそらくあなた自身が、上司のコーディングスタイルを追おうとしているときに、ロジックバグをそこに入れるでしょう。
私はまた、ここでロバーツの論文は平均的なプログラマにとってはるかにアクセスしやすいので、Knuthの論文よりも最初に読む方がよいと言います。また、短く、より狭いトピックをカバーしています。彼がCSタイプではなく経営者である場合でも、おそらくそれを上司にすすめることもできます。
私はこれらの悪い習慣のいずれかを使用することを検討していませんが、同じループ内でそれらを使いすぎると、ループで使用されているロジックを再考する必要があります。控えめに使用してください。
あなたが与えた例は休憩も継続も必要ありません:
while (primary-condition AND
loop-count <= 1000 AND
time-exec <= 3600) {
when (data != "undefined" AND
NOT skip)
do-something-useful;
}
あなたの例の4行の私の「問題」は、それらがすべて同じレベルにあるが、異なることをするということです。
私のネストされたアプローチでは、深く行くほど、コードはより「有用」になります。
しかし、ループの奥深くで(primary-condition以外の)ループを停止する理由を見つけた場合は、ブレークまたはリターンで使用できます。トップレベルの条件でテストされる追加のフラグを使用するよりも、そうしたいと思います。ブレーク/リターンはより直接的です。さらに別の変数を設定するよりも、意図を明確に示します。
「悪さ」は、それらをどのように使用するかに依存します。アルゴリズムのリファクタリングでは保存できないサイクルを節約できる場合にのみ、ループ構造でブレークを通常使用します。たとえば、特定のプロパティの値がtrueに設定されているアイテムを探すコレクションを循環します。知っておく必要があるのは、項目の1つでこのプロパティがtrueに設定されていることだけである場合、その結果が得られたら、ループを適切に終了するためのブレークが適切です。
ブレークを使用してもコードが明確に読みやすくなり、実行時間が短くなり、処理のサイクルを大幅に節約できない場合は、コードを使用しないことをお勧めします。私は可能な限り「最低公約数」にコーディングする傾向があります。これは、私をフォローしている人が誰でも簡単に私のコードを見て、何が起こっているかを理解できるようにするためです(私はこれで常に成功するとは限りません)。ブレークは、奇妙な入口/出口ポイントを導入するので、それを減らします。誤用された場合、それらは「後藤」からの抜粋のように振る舞うことができます。
絶対にそうではありません...はい、goto
の使用は、プログラムの構造を劣化させ、制御フローを理解するのが非常に難しいため、不適切です。
しかし、break
やcontinue
のようなステートメントの使用は、今日では絶対に必要であり、プログラミングの悪い習慣とはまったく見なされていません。
また、break
とcontinue
を使用する場合の制御フローを理解することはそれほど難しくありません。 switch
のような構成では、break
ステートメントが絶対に必要です。
2番目のコードスニペットを次のように置き換えます
while (primary_condition && (loop_count <= 1000 && time_exect <= 3600)) {
if (this->data != "undefined" && this->skip != true) {
..
}
}
簡潔さの理由ではありません-これは実際に読みやすく、誰かが何が起こっているのかを理解する方が簡単だと思います。一般的に言えば、ループの状態は、体全体に散らばっていないループ状態内に純粋に含まれるべきです。ただし、break
とcontinue
が読みやすくなる状況もあります。 break
moreso continue
次より追加するかもしれません:D
本質的な概念は、プログラムを意味的に分析できることです。 1つのエントリと1つの出口がある場合、可能な状態を示すために必要な計算は、分岐パスを管理する必要がある場合よりもはるかに簡単です。
一部には、この難しさは、コードについて概念的に推論できることに反映されています。
率直に言って、2番目のコードは明白ではありません。何してるの?続行しますか、それともループの「次」になりますか?何も思いつきません。少なくとも最初の例は明らかです。
私はあなたの上司に同意します。それらは、サイクロマティックの複雑性が高いメソッドを生成するため、不良です。そのような方法は読みにくく、テストが難しい。幸いなことに、簡単な解決策があります。ループ本体を別のメソッドに抽出します。ここで、「続行」は「リターン」になります。 「返品」の方が良いのは、「返品」の後に終わったからです。地方の状態についての心配はありません。
「break」の場合、ループ自体を別のメソッドに抽出し、「break」を「return」に置き換えます。
抽出されたメソッドが多数の引数を必要とする場合、それはクラスを抽出することを示しています。それらをコンテキストオブジェクトに収集します。
私はあなたの上司に同意しません。 break
とcontinue
を使用する適切な場所があります。実際、例外と例外処理が最新のプログラミング言語に導入されたのは、structured techniques
だけではすべての問題を解決できないためです。
余談ここで宗教的な議論を始めたくありませんが、次のようにコードをさらに読みやすくするようにコードを再構成できます。
while (primary_condition) {
if (loop_count > 1000) || (time_exect > 3600) {
break;
} else if ( ( this->data != "undefined") && ( !this->skip ) ) {
... // where the real work of the loop happens
}
}
反対側の注記
個人的に条件付きでの( flag == true )
の使用が嫌いです。なぜなら、変数がすでにブール値である場合、ブール値が必要な答えを持っているときに実行する必要がある追加の比較を導入しているからです。コンパイラーがその余分な比較を最適化することを確実にします。
私は原則としてcontinue
とbreak
に反対しているわけではありませんが、非常に低レベルのコンストラクトであり、多くの場合、さらに優れたものに置き換えることができます。
ここでは例としてC#を使用しています。コレクションを反復処理する場合を考えてみますが、必要なのはいくつかの述語を満たす要素だけであり、最大100回を超える反復は行いません。
for (var i = 0; i < collection.Count; i++)
{
if (!Predicate(item)) continue;
if (i >= 100) break; // at first I used a > here which is a bug. another good thing about the more declarative style!
DoStuff(item);
}
これは合理的にきれいに見えます。理解するのはそれほど難しいことではありません。私はそれがより宣言的であることから多くを得るために立つと思います。以下と比較してください。
foreach (var item in collection.Where(Predicate).Take(100))
DoStuff(item);
おそらく、WhereとTakeの呼び出しは、このメソッドに含めるべきではありません。おそらく、このフィルタリングは、コレクションがこのメソッドに渡される前に行う必要があります。とにかく、低レベルのものから離れて、実際のビジネスロジックにさらに焦点を合わせると、実際に何に関心があるのかがより明確になります。コードを、優れた設計プラクティスに忠実なまとまりのあるモジュールに分離することが容易になります。オン。
低レベルのものはコードの一部にまだ存在しますが、ビジネスの問題を推論するために使用できる精神的なエネルギーを必要とするため、これをできるだけ隠す必要があります。
複数のループ内に深くネストされている場合にのみ問題になると思います。どのループにブレークしているのかを知るのは困難です。コンティニューに従うことも難しいかもしれませんが、本当の苦痛は休憩から来ると思います-ロジックに従うことは難しい場合があります。
どちらのスタイルも好きではありません。これが私が好むものです:
function verify(object)
{
if not (object->value < 0)
and not(object->value > object->max_value)
and not(object->name == "")
{
do somethign important
}
else return false; //probably not necessary since this function doesn't even seem to be defined to return anything...?
}
関数を中止するためにreturn
を使用することは本当に好きではありません。 return
の乱用のようです。
break
の使用も、必ずしも明確であるとは限りません。
より良いかもしれません:
notdone := primarycondition
while (notDone)
{
if (loop_count > 1000) or (time_exect > 3600)
{
notDone := false;
}
else
{
skipCurrentIteration := (this->data == "undefined") or (this->skip == true)
if not skipCurrentIteration
{
do something
}
...
}
}
ネストが少なくなり、複雑な条件が変数にリファクタリングされます(実際のプログラムでは、明らかにより良い名前にする必要があります...)
(上記のコードはすべて疑似コードです)
No。これは問題を解決する方法であり、他にも解決する方法があります。
現在の多くの主流言語(Java、.NET(C#+ VB)、PHP、独自の言語を記述する)では、「break」と「continue」を使用してループをスキップしています。どちらも「構造化goto(s)」文です。
彼らがいなければ:
String myKey = "mars";
int i = 0; bool found = false;
while ((i < MyList.Count) && (not found)) {
found = (MyList[i].key == myKey);
i++;
}
if (found)
ShowMessage("Key is " + i.toString());
else
ShowMessage("Not found.");
彼らと:
String myKey = "mars";
for (i = 0; i < MyList.Count; i++) {
if (MyList[i].key == myKey)
break;
}
ShowMessage("Key is " + i.toString());
「break」と「continue」のコードは短く、通常「while」文を「for」または「foreach」文に変換することに注意してください。
どちらの場合もコーディングスタイルの問題です。 私はそれらを使用したくない。詳細なスタイルでコードをより詳細に制御できるため。
私は実際、いくつかのプロジェクトで働いており、それらの文を使用することが義務付けられていました。
一部の開発者は、それらを必然的ではないと考えているかもしれませんが、仮説上、それらを削除しなければならなかった場合は、「while」と「do while」(「Repeat until」、Pascalの皆さん)も削除する必要があります;-)
結論、私がそれらを使用しないことを好むとしても、私はそれはオプションであり、悪いプログラミング慣行ではないと思います。
次の例のように、それらが偽のgotoとして使用されていない限り:
do
{
if (foo)
{
/*** code ***/
break;
}
if (bar)
{
/*** code ***/
break;
}
} while (0);
元気です。 (製品コードで見た例、meh)