私はよく「同じメソッドに複数のreturnステートメントを入れないでください。」と言うプログラマーと話します。理由を教えてくれるように依頼すると、私は「コーディング標準はそう言っています。 "または" それは混乱します。 "単一のreturnステートメントで解決策を示したとき、コードは私には醜く見えます。例えば:
if (condition)
return 42;
else
return 97;
"これは醜いです。ローカル変数を使用する必要があります!"
int result;
if (condition)
result = 42;
else
result = 97;
return result;
この50%のコード膨張により、プログラムはどのように理解しやすくなりますか?個人的には、簡単に防ぐことができる別の変数によって状態空間が増加しただけなので、私はそれが難しいと感じます。
もちろん、通常は次のように記述します。
return (condition) ? 42 : 97;
しかし、多くのプログラマーは条件演算子を避け、長い形式を好みます。
この「1回のみ」という概念はどこから来たのですか。この条約が生まれた歴史的な理由はありますか?
「単一エントリ、単一出口」は、ほとんどのプログラミングがアセンブリ言語、FORTRAN、またはCOBOLで行われたときに作成されました。現代の言語はダイクストラが警告していた慣行をサポートしていないため、広く誤解されてきました。
「単一エントリ」は、「関数の代替エントリポイントを作成しない」ことを意味しました。もちろん、アセンブリ言語では、任意の命令で関数を入力できます。 FORTRANはENTRY
ステートメントを使用して関数への複数のエントリをサポートしました:
SUBROUTINE S(X, Y)
R = SQRT(X*X + Y*Y)
C ALTERNATE ENTRY USED WHEN R IS ALREADY KNOWN
ENTRY S2(R)
...
RETURN
END
C USAGE
CALL S(3,4)
C ALTERNATE USAGE
CALL S2(5)
「単一の出口」とは、関数がtoを1か所だけ返すことを意味します:呼び出しの直後のステートメント。 notは、関数がfromを1つだけ返すことを意味しました。 構造化プログラミングが作成されたとき、関数が別の場所に戻ってエラーを示すのは一般的な方法でした。 FORTRANは、「代替リターン」を介してこれをサポートしました。
C SUBROUTINE WITH ALTERNATE RETURN. THE '*' IS A PLACE HOLDER FOR THE ERROR RETURN
SUBROUTINE QSOLVE(A, B, C, X1, X2, *)
DISCR = B*B - 4*A*C
C NO SOLUTIONS, RETURN TO ERROR HANDLING LOCATION
IF DISCR .LT. 0 RETURN 1
SD = SQRT(DISCR)
DENOM = 2*A
X1 = (-B + SD) / DENOM
X2 = (-B - SD) / DENOM
RETURN
END
C USE OF ALTERNATE RETURN
CALL QSOLVE(1, 0, 1, X1, X2, *99)
C SOLUTION FOUND
...
C QSOLVE RETURNS HERE IF NO SOLUTIONS
99 PRINT 'NO SOLUTIONS'
これらの手法はどちらもエラーが発生しやすいものでした。代替エントリを使用すると、一部の変数が初期化されないままになることがよくあります。代替リターンを使用すると、GOTOステートメントのすべての問題が発生し、分岐条件が分岐に隣接しておらず、サブルーチンのどこかにあるという複雑さが加わりました。
このSingle Entry、Single Exit(SESE)の概念は、明示的なリソース管理を伴う言語に由来します、Cやアセンブリと同様。 Cでは、次のようなコードはリソースをリークします。
void f()
{
resource res = acquire_resource(); // think malloc()
if( f1(res) )
return; // leaks res
f2(res);
release_resource(res); // think free()
}
そのような言語では、基本的に3つのオプションがあります。
クリーンアップコードを複製します。
うーん。冗長性は常に悪いです。
クリーンアップコードにジャンプするには、goto
を使用します。
これには、関数の最後にクリーンアップコードが必要です。 (そして、これがgoto
がその場所にあると主張する理由です。そして、それは確かに– Cで。)
ローカル変数を導入し、それを介して制御フローを操作します。
デメリットは、構文によって操作される制御フロー(break
、return
、if
、while
を考える)は、制御フローは変数の状態によって操作されます(アルゴリズムを見ると、これらの変数には状態がないため)。
アセンブリでは、関数を呼び出すときに関数の任意のアドレスにジャンプできるため、さらに奇妙です。これは、任意の関数へのエントリポイントの数がほぼ無制限であることを意味します。 (これが役立つ場合もあります。このようなサンクは、C++での複数継承シナリオでthis
関数を呼び出すために必要なvirtual
ポインター調整を実装するコンパイラーの一般的な手法です。)
リソースを手動で管理する必要がある場合、任意の場所で関数を開始または終了するオプションを利用すると、コードがより複雑になり、バグが発生します。したがって、よりクリーンなコードとより少ないバグを取得するために、SESEを伝播する一連の考え方が現れました。
ただし、言語が例外を備えている場合、(ほぼ)すべての関数が(ほぼ)任意の時点で時期尚早に終了する可能性があるため、いずれにせよ、時期尚早の復帰に備える必要があります。 (finally
は主にJavaとusing
で使用されると思います(IDisposable
を実装する場合、それ以外の場合はfinally
) C#では、C++は代わりに [〜#〜] raii [〜#〜] を使用します。これを実行すると、クリーンアップに失敗することはできません初期のreturn
ステートメントのために自分自身の後で、SESEを支持するおそらく最も強力な議論は消滅しました。
それは読みやすさを残します。もちろん、200個のLoC関数に6個以上のreturn
ステートメントをランダムに散布したものは、プログラミングスタイルが良くなく、コードを読みやすくするものではありません。しかし、そのような関数は、それらの時期尚早の戻り値なしでも理解するのは容易ではありません。
リソースを手動で管理しない、または管理するべきではない言語では、古いSESEの慣習を守る価値はほとんどありません。 OTOH、私が上で議論したように、SESEはしばしばコードをより複雑にします。これは恐竜であり(Cを除く)、今日のほとんどの言語にうまく適合しません。コードの理解を助けるのではなく、それはそれを妨げます。
Javaプログラマーがこれに固執するのはなぜですか?わかりませんが、私の(外部の)POVから、JavaはCから多くの規則を取りました(ここでそれらは理にかなっている)そしてそれらをそのOO=世界(それらは役に立たないか、完全に悪い場所)に適用しました、どこでそれは今どんなコストにも関わらずそれらに固執します。(すべてを定義する慣習のようにスコープの先頭にある変数)
プログラマーは、不合理な理由により、あらゆる種類の奇妙な表記法を使用します。 (深く入れ子になった構造ステートメント–「矢じり」– Pascalなどの言語では、かつては美しいコードと見なされていました。)これに純粋な論理的推論を適用すると、確立された方法から逸脱するように説得することができないようです。そのような習慣を変える最善の方法は、おそらく従来のことではなく、最善を尽くすことを早い段階で教えることでしょう。あなたはプログラミングの教師であり、あなたの手にそれを持っています。 :)
一方では、単一のreturnステートメントにより、ロギングが容易になり、ロギングに依存するデバッグの形式も簡単になります。単一のポイントで戻り値を出力するためだけに、関数を単一の戻り値に削減しなければならなかったことがよくあります。
_ int function() {
if (bidi) { print("return 1"); return 1; }
for (int i = 0; i < n; i++) {
if (vidi) { print("return 2"); return 2;}
}
print("return 3");
return 3;
}
_
一方、これをfunction()
にリファクタリングして、_function()
を呼び出し、結果をログに記録することができます。
「シングルエントリ、シングルエクジット」は、1970年代初頭の構造化プログラミング革命から始まりました。この革命は、編集者へのEdsger W. Dijkstraからの手紙「 GOTOステートメントは有害と見なされます 」から始まりました。構造化プログラミングの背後にある概念は、Ole Johan-Dahl、Edsger W. Dijkstra、およびCharles Anthony Richard Hoareによる古典的な本「Structured Programming」で詳細に説明されています。
「有害なGOTO声明」は、今日でも読む必要があります。 "Structured Programming"は古くなっていますが、それでも非常にやりがいがあり、開発者の "Must Read"リストの一番上にあるはずです。スティーブ・マコーネル。 (Dahlのセクションでは、C++のクラスとすべてのオブジェクト指向プログラミングの技術的基盤であるSimula 67のクラスの基本を説明しています。)
ファウラーをリンクするのはいつでも簡単です。
SESEに反する主な例の1つがガード句です。
すべての特殊なケースでガード句を使用する
double getPayAmount() { double result; if (_isDead) result = deadAmount(); else { if (_isSeparated) result = separatedAmount(); else { if (_isRetired) result = retiredAmount(); else result = normalPayAmount(); }; } return result; };
double getPayAmount() { if (_isDead) return deadAmount(); if (_isSeparated) return separatedAmount(); if (_isRetired) return retiredAmount(); return normalPayAmount(); };
詳細については、リファクタリングの250ページを参照してください...
要するに、このルールはガベージコレクションや例外処理を持たない言語の時代から来ているということです。このルールが現代の言語でより良いコードにつながることを示す正式な研究はありません。これによりコードが短くなったり読みやすくなったりする場合は、いつでも無視してください。 Javaこれを主張している人たちは、時代遅れの無意味なルールに従って盲目的にそして疑いなくしています。
1つのリターンにより、リファクタリングが容易になります。 return、break、またはcontinueを含むforループの内部本体に対して「extractメソッド」を実行してみてください。制御フローが壊れているため、これは失敗します。
要点は、完璧なコードを書くふりをしている人はいないと思います。そのため、コードは常に「改善」および拡張されるようにリファクタリングされています。したがって、私の目標は、コードをできるだけリファクタリングに適した状態に保つことです。
制御フローブレーカーが含まれている場合や、機能を少しだけ追加したい場合、関数を完全に再定式化しなければならないという問題にしばしば直面します。分離されたネストに新しいパスを導入する代わりに、制御フロー全体を変更すると、これは非常にエラーが発生しやすくなります。最後に単一の戻り値が1つしかない場合、またはガードを使用してループを終了する場合は、当然、ネストとコードが多くなります。しかし、コンパイラとIDEサポートされているリファクタリング機能を利用できます。
複数のreturnステートメントが単一のreturnステートメントにGOTOを持っていることと同等であるという事実を考慮してください。これは、breakステートメントの場合と同じです。したがって、私と同じように、すべての意図と目的のためにGOTOだと考える人もいます。
ただし、これらのタイプのGOTOは有害であるとは考えていません。正当な理由が見つかった場合は、コードで実際のGOTOを使用することをためらうことはありません。
私の一般的なルールは、GOTOはフロー制御専用であることです。ループに使用したり、「上方向」または「逆方向」にGOTOしたりしないでください。 (これは、ブレーク/リターンが機能する方法です)
他の人が述べたように、以下は必読です GOTOステートメントは有害と見なされます
ただし、これはGOTOが過度に使用された1970年に書かれたことを覚えておいてください。すべてのGOTOが有害であるとは限りません。通常の構成の代わりに使用しない限り、私はそれらの使用を思いとどまらせません。むしろ、通常の構成を使用することが非常に不便になるという奇妙なケースです。
通常の場合には発生しないはずの障害のために領域をエスケープする必要があるエラーの場合にそれらを使用すると便利なことがあります。ただし、GOTOを使用する代わりに早期に戻ることができるように、このコードを別の関数に配置することも検討する必要があります...しかし、これも不便な場合があります。
SonarCubeが循環的複雑度を決定するために複数のreturnステートメントを使用するのを見てきました。したがって、returnステートメントが多いほど、循環的複雑度は高くなります
複数の戻り値とは、戻り値の型を変更する場合、関数の複数の場所で変更する必要があることを意味します。
戻り値の原因を理解するために条件文と併せてロジックを注意深く検討する必要があるため、デバッグは困難です。
複数のreturnステートメントの解決策は、必要な実装オブジェクトを解決した後で、それらをポリモーフィズムに置き換えることで、単一のreturnを返すことです。