関数内に return ステートメントを 1 つだけ持つ方がよい理由はありますか?
それとも、関数内に多くの return ステートメントが存在する可能性があることを意味する、論理的に正しいとすぐに関数から戻ることは問題ないのでしょうか?
関数内に return ステートメントを 1 つだけ持つ方がよい理由はありますか?
それとも、関数内に多くの return ステートメントが存在する可能性があることを意味する、論理的に正しいとすぐに関数から戻ることは問題ないのでしょうか?
「簡単な」状況に戻るために、メソッドの開始時にいくつかのステートメントを使用することがよくあります。たとえば、次のようになります。
public void DoStuff(Foo foo)
{
if (foo != null)
{
...
}
}
...次のように読みやすくすることができます(私見):
public void DoStuff(Foo foo)
{
if (foo == null) return;
...
}
はい、関数/メソッドから複数の「終了ポイント」があっても問題ないと思います。
Code Completeについては誰も言及も引用もしていないので、私がやります。
各ルーチンのリターンの数を最小限に抑えます。ルーチンの下部を読んで、それが上のどこかに戻ってきた可能性に気付いていない場合、ルーチンを理解するのは難しくなります。
読みやすさが向上する場合は、リターンを使用します。特定のルーチンでは、答えがわかったら、すぐに呼び出し元のルーチンに戻したいと考えています。ルーチンがクリーンアップを必要としないように定義されている場合、すぐに戻らないということは、さらにコードを作成する必要があることを意味します。
複数の出口点に対して恣意的に決定することは信じられないほど賢明ではありません。なぜなら、この手法が実際に何度も何度も役立つことがわかったからです。実際、明確にするために、既存のコードを複数の出口点にリファクタリングすることがよくあります。このように、2 つのアプローチを比較できます。
string fooBar(string s, int? i) {
string ret = "";
if(!string.IsNullOrEmpty(s) && i != null) {
var res = someFunction(s, i);
bool passed = true;
foreach(var r in res) {
if(!r.Passed) {
passed = false;
break;
}
}
if(passed) {
// Rest of code...
}
}
return ret;
}
これを、複数の終了ポイントが許可されているコードと比較してください:-
string fooBar(string s, int? i) {
var ret = "";
if(string.IsNullOrEmpty(s) || i == null) return null;
var res = someFunction(s, i);
foreach(var r in res) {
if(!r.Passed) return null;
}
// Rest of code...
return ret;
}
後者の方がかなりわかりやすいと思います。私が知る限り、複数の出口点に対する批判は、最近ではかなり古風な見方です。
私は現在、コードベースに取り組んでいる2人の人々が盲目的に「単一の出口」理論に同意しています。経験から、それは恐ろしい恐ろしい慣習であると言えます。コードの保守が非常に難しくなるため、その理由を説明します。
「単一の出口」理論では、必然的に次のようなコードになります。
function()
{
HRESULT error = S_OK;
if(SUCCEEDED(Operation1()))
{
if(SUCCEEDED(Operation2()))
{
if(SUCCEEDED(Operation3()))
{
if(SUCCEEDED(Operation4()))
{
}
else
{
error = OPERATION4FAILED;
}
}
else
{
error = OPERATION3FAILED;
}
}
else
{
error = OPERATION2FAILED;
}
}
else
{
error = OPERATION1FAILED;
}
return error;
}
これにより、コードを追跡するのが非常に難しくなるだけでなく、後で戻って1から2の間に操作を追加する必要があると言います。おかしな関数のほぼ全体をインデントする必要があります。 if/else条件と中括弧が適切に一致しています。
この方法では、コードの保守が非常に困難になり、エラーが発生しやすくなります。
構造化プログラミングでは、関数ごとに return ステートメントを 1 つだけ使用する必要があります。これは、複雑さを制限するためです。Martin Fowler などの多くの人々は、複数の return ステートメントを使用して関数を記述する方が簡単であると主張しています。彼は、彼が書いた古典的なリファクタリングの本でこの議論を提示しています。これは、彼の他のアドバイスに従い、小さな関数を作成する場合にうまく機能します。私はこの観点に同意し、厳格な構造化プログラミングの純粋主義者だけが、関数ごとに単一の return ステートメントに固執します。
Kent Beck が実装パターンでガード句について議論する際に指摘しているように、ルーチンのエントリ ポイントとエグジット ポイントは 1 つです ...
「同じルーチン内の多くの場所にジャンプしたり、ジャンプしたりするときに起こりうる混乱を防ぐためでした。どのステートメントが実行されたかを理解することさえ困難な、多くのグローバルデータで書かれたFORTRANまたはアセンブリ言語プログラムに適用すると、それは理にかなっています.. . 小さなメソッドと大部分がローカル データの場合、不必要に保守的です。」
ガード句を使用して記述された関数は、長くネストされた一連のif then else
ステートメントよりもはるかに簡単に理解できます。
副作用のない関数では、複数の戻り値を持つ正当な理由はなく、それらを関数型スタイルで記述する必要があります。副作用のあるメソッドでは、物事はよりシーケンシャル (時間インデックス) になるため、実行を停止するコマンドとして return ステートメントを使用して、命令型スタイルで記述します。
つまり、可能であれば、このスタイルを優先してください
return a > 0 ?
positively(a):
negatively(a);
この上に
if (a > 0)
return positively(a);
else
return negatively(a);
ネストされた条件の複数のレイヤーを記述していることに気付いた場合は、たとえば述語リストを使用して、おそらくそれをリファクタリングできる方法があります。if と else が構文的に大きく離れていることがわかった場合は、それをより小さな関数に分割することをお勧めします。画面いっぱいのテキストを超える条件付きブロックは読みにくいです。
すべての言語に適用される厳格なルールはありません。return ステートメントが 1 つしかないようなものでは、コードがうまくいきません。しかし、良いコードは、関数をそのように書くことを可能にする傾向があります。
C のハングオーバーである C++ のコーディング標準で見たことがあります。RAII やその他の自動メモリ管理がない場合は、リターンごとにクリーンアップする必要があります。つまり、カットアンドペーストを意味します。クリーンアップまたは goto (マネージド言語の「最終的に」と論理的に同じ) のどちらも悪い形式と見なされます。C++ やその他の自動メモリ システムでスマート ポインターとコレクションを使用する慣行がある場合、それには強い理由はなく、可読性がすべてであり、判断が必要になります。
私は、関数の途中にある return ステートメントは良くないという考えに傾いています。return を使用して、関数の先頭にいくつかのガード句を作成し、もちろんコンパイラに関数の最後に何を返すかを問題なく伝えることができますが、関数の途中での return は見逃されやすく、関数の解釈を難しくします。
単一の終了点を持つことは、関数の最後に単一のブレークポイントを設定して、実際に返される値を確認できるため、デバッグに有利です。
一般的に、私は関数からの出口点を1つだけ持つようにしています。ただし、実際にそうすると、必要以上に複雑な関数本体が作成される場合があります。その場合は、複数の出口点を使用することをお勧めします。結果として生じる複雑さに基づいた「判断の呼びかけ」である必要がありますが、目標は、複雑さと理解しやすさを犠牲にすることなく、可能な限り少ない出口点である必要があります。
本当に複雑でない限り、私の好みは単一の出口です。場合によっては、複数の存在ポイントが他のより重要な設計上の問題を覆い隠す可能性があることがわかりました。
public void DoStuff(Foo foo)
{
if (foo == null) return;
}
このコードを見て、私はすぐに次のように尋ねます。
これらの質問への回答によっては、
上記のどちらの場合でも、'foo' が null にならず、関連する呼び出し元が変更されないように、アサーションを使用してコードを書き直すことができます。
複数の存在が実際に悪影響を与える可能性がある他の2つの理由(C++コードに固有だと思います)があります。それらはコードサイズとコンパイラの最適化です。
関数の出口でスコープ内にある非 POD C++ オブジェクトでは、そのデストラクタが呼び出されます。複数の return ステートメントがある場合、スコープ内に異なるオブジェクトが存在する可能性があるため、呼び出すデストラクタのリストが異なります。したがって、コンパイラは return ステートメントごとにコードを生成する必要があります。
void foo (int i, int j) {
A a;
if (i > 0) {
B b;
return ; // Call dtor for 'b' followed by 'a'
}
if (i == j) {
C c;
B b;
return ; // Call dtor for 'b', 'c' and then 'a'
}
return 'a' // Call dtor for 'a'
}
コード サイズが問題になる場合、これは避ける価値があるかもしれません。
もう 1 つの問題は、"Named Return Value OptimiZation" (別名 Copy Elision、ISO C++ '03 12.8/15) に関連しています。C++ では、次のことが可能な場合、実装でコピー コンストラクターの呼び出しをスキップできます。
A foo () {
A a1;
// do something
return a1;
}
void bar () {
A a2 ( foo() );
}
コードをそのまま使用すると、オブジェクト「a1」が「foo」で構築され、そのコピー構造が呼び出されて「a2」が構築されます。ただし、コピー省略により、コンパイラはスタック上の「a2」と同じ場所に「a1」を構築できます。したがって、関数が戻るときにオブジェクトを「コピー」する必要はありません。
複数の出口点があると、これを検出しようとするコンパイラーの作業が複雑になります。少なくとも比較的最近のバージョンの VC++ では、関数本体に複数の戻り値がある場合、最適化は行われませんでした。詳細については、Visual C++ 2005 での名前付き戻り値の最適化を参照してください。
いいえ、私たちはもう 1970 年代に住んでいないからです。関数が長すぎて複数の戻り値が問題になる場合は、長すぎます。
(例外のある言語の複数行関数には複数の出口点があるという事実は別として。)
出口点が 1 つであると循環的複雑度が低下するため、理論上は、コードを変更したときにコードにバグが入り込む可能性が低くなります。しかし実際には、より実用的なアプローチが必要であることを示唆する傾向があります。したがって、私は単一の出口点を持つことを目指す傾向がありますが、コードがより読みやすい場合は、複数の出口点を持つことができます。
return
ある意味でコード臭が発生するため、ステートメントを1 つだけ使用するようにしています。説明させてください:
function isCorrect($param1, $param2, $param3) {
$toret = false;
if ($param1 != $param2) {
if ($param1 == ($param3 * 2)) {
if ($param2 == ($param3 / 3)) {
$toret = true;
} else {
$error = 'Error 3';
}
} else {
$error = 'Error 2';
}
} else {
$error = 'Error 1';
}
return $toret;
}
(条件は任意ですが…)
条件が多いほど、関数が大きくなり、読み取りが難しくなります。したがって、コードのにおいに慣れている場合は、それに気づき、コードをリファクタリングしたくなるでしょう。考えられる解決策は次の 2 つです。
複数の返品
function isCorrect($param1, $param2, $param3) {
if ($param1 == $param2) { $error = 'Error 1'; return false; }
if ($param1 != ($param3 * 2)) { $error = 'Error 2'; return false; }
if ($param2 != ($param3 / 3)) { $error = 'Error 3'; return false; }
return true;
}
セパレート機能
function isEqual($param1, $param2) {
return $param1 == $param2;
}
function isDouble($param1, $param2) {
return $param1 == ($param2 * 2);
}
function isThird($param1, $param2) {
return $param1 == ($param2 / 3);
}
function isCorrect($param1, $param2, $param3) {
return !isEqual($param1, $param2)
&& isDouble($param1, $param3)
&& isThird($param2, $param3);
}
確かに、それは長くて少し面倒ですが、この方法で関数をリファクタリングする過程で、
結果として生じる必然的な「矢印」プログラミングについて言うのは悪いことであるのと同じように、単一の出口点を持つことについて言うのは良いことです。
入力検証またはリソース割り当て中に複数の出口点を使用する場合は、すべての「エラー出口」を関数の上部に非常に目立つように配置しようとします。
「SSDSLPedia」のSpartanプログラミングの記事と「PortlandPatternRepository'sWiki」の単一機能の出口点の記事の両方に、これに関するいくつかの洞察に満ちた議論があります。また、もちろん、考慮すべきこの投稿があります。
たとえば、リソースを1つの場所で解放するために、(例外が有効になっていない言語で)単一の出口点が本当に必要な場合は、gotoを注意深く適用するのが適切だと思います。たとえば、このかなり不自然な例を参照してください(画面の領域を保存するために圧縮されています)。
int f(int y) {
int value = -1;
void *data = NULL;
if (y < 0)
goto clean;
if ((data = malloc(123)) == NULL)
goto clean;
/* More code */
value = 1;
clean:
free(data);
return value;
}
個人的には、一般的に、複数の出口点を嫌うよりも矢印プログラミングが嫌いですが、どちらも正しく適用すると便利です。もちろん、最善の方法は、どちらも必要としないようにプログラムを構成することです。関数を複数のチャンクに分割すると、通常は役立ちます:)
そうすると、この例のように、とにかく複数の出口点ができてしまいます。ここでは、いくつかの大きな関数がいくつかの小さな関数に分割されています。
int g(int y) {
value = 0;
if ((value = g0(y, value)) == -1)
return -1;
if ((value = g1(y, value)) == -1)
return -1;
return g2(y, value);
}
プロジェクトまたはコーディングガイドラインによっては、ボイラープレートコードのほとんどをマクロに置き換えることができます。ちなみに、このように分解すると、関数g0、g1、g2を個別にテストするのが非常に簡単になります。
明らかに、OOおよび例外対応言語では、そのようなifステートメントを使用しません(または、十分な労力でそれを回避できれば)、コードははるかにわかりやすくなります。そして、非矢印。そして、非最終的なリターンのほとんどはおそらく例外でしょう。
要するに;
必要な数だけ、またはコードをよりクリーンにするもの(ガード句など)を用意する必要があると思います。
個人的には、return ステートメントは 1 つだけにするべきだと言っている「ベスト プラクティス」を聞いたり見たりしたことがありません。
ほとんどの場合、私は論理パスに基づいてできるだけ早く関数を終了する傾向があります (ガード句はこの良い例です)。
通常、複数のリターンが良いと思います (C# で記述したコードでは)。シングル リターン スタイルは C からの継承です。しかし、C でコーディングしていない可能性があります。
すべてのプログラミング言語で、1 つのメソッドに対して 1 つの出口点のみを要求する法律はありません。このスタイルの優位性を主張する人もいれば、「ルール」や「法律」にまで引き上げることもありますが、この信念は証拠や研究によって裏付けられていません.
try..finally
複数の戻りスタイルは、リソースを明示的に割り当て解除する必要がある C コードでは悪い習慣かもしれませんが、自動ガベージ コレクションやブロック (およびusing
C# のブロック)などの構造を持つ Java、C#、Python、JavaScript などの言語では、)、そしてこの議論は当てはまりません - これらの言語では、集中化された手動のリソース割り当て解除が必要になることは非常にまれです。
単一のリターンの方が読みやすい場合とそうでない場合があります。コードの行数を減らすか、ロジックをより明確にするか、中括弧とインデントまたは一時変数の数を減らすかを確認してください。
したがって、技術的な問題ではなく、レイアウトと読みやすさの問題であるため、芸術的な感性に合わせてできるだけ多くのリターンを使用してください。
これについては、ブログで詳しく説明しています。
あなたは格言を知っています -美しさは見る人の目にあります。
NetBeansを信頼する人もいれば、IntelliJ IDEAを信頼する人、Pythonを信頼する人、PHPを信頼する人もいます。
一部の店舗では、これを行うことを主張すると、仕事を失う可能性があります。
public void hello()
{
if (....)
{
....
}
}
問題は、可視性と保守性です。
私はブール代数を使用してロジックを削減および簡素化し、ステート マシンを使用することに夢中になっています。しかし、以前の同僚の中には、私のコード作成における「数学的手法」の採用は、目に見えず、保守も容易ではないため、不適切であると考えていました。そして、それは悪い習慣です。申し訳ありませんが、私が採用している手法は非常に目に見えて維持しやすいものです。なぜなら、6 か月後にコードに戻ったときに、よくあるスパゲッティの混乱を見るよりも、コードを明確に理解できるからです。
やあ相棒 (以前のクライアントが言っていたように) 私があなたにそれを直す必要があるとき、あなたがそれを直す方法を知っている限り、あなたが望むことをしてください.
20 年前、私の同僚が今日アジャイル開発戦略と呼ばれるものを採用したために解雇されたことを覚えています。彼は綿密な増分計画を立てていました。しかし、彼のマネージャーは彼に怒鳴りつけていました。マネージャーに対する彼の反応は、段階的な開発が顧客のニーズにより正確になるというものでした。彼は顧客のニーズに合わせて開発することを信じていましたが、マネージャーは「顧客の要求」に合わせてコーディングすることを信じていました。
私たちは、データの正規化、 MVP、MVCの境界を破ったことで罪を犯すことがよくあります。関数を構築する代わりにインライン化します。近道をします。
個人的には、PHP は悪い習慣だと思いますが、私は何を知っていますか。すべての理論的議論は、1 つのルール セットを満たそうとすることに要約されます。
品質 = 精度、保守性、収益性。
他のすべてのルールはバックグラウンドにフェードインします。そしてもちろん、このルールは決して衰えることはありません:
怠惰は優れたプログラマーの美徳です。
複数の出口点は、十分に小さい関数、つまり、1つの画面の長さ全体で表示できる関数には問題ありません。長い関数に同様に複数の出口点が含まれている場合は、関数をさらに細かく切り刻むことができることを示しています。
とはいえ、どうしても必要な場合を除いて、複数出口機能は避けています。私は、より複雑な関数のいくつかのあいまいな行でのいくつかの漂遊リターンに起因するバグの痛みを感じました。
私が考えることができる正当な理由の 1 つは、コードのメンテナンスのためです。単一の出口があります。結果のフォーマットを変更したい場合は...、実装がはるかに簡単です。また、デバッグのために、そこにブレークポイントを貼り付けることができます:)
そうは言っても、コーディング標準が「関数ごとに1つのreturnステートメント」を課すライブラリで作業しなければならなかったことがありますが、それはかなり大変でした。私は多くの数値計算コードを書いていますが、「特殊なケース」がしばしばあるため、コードを理解するのは非常に困難でした...
私の通常のポリシーは、コードを追加してコードの複雑さが大幅に軽減されない限り、関数の最後に return ステートメントを 1 つだけ持つことです。実際、私はむしろ Eiffel のファンです。Eiffel は、return ステートメントを持たないことで唯一の return ルールを適用します (結果を入れるための自動作成された「結果」変数があるだけです)。
確かに、複数のリターンを使用することで、それらを使用しない明らかなバージョンよりもコードを明確にすることができる場合があります。複雑すぎて複数の return ステートメントがないと理解できない関数がある場合は、さらに手直しが必要であると主張する人もいるかもしれませんが、そのようなことについて実用的であることが良い場合もあります。
私はあなたに単一の出口パスを強制するひどいコーディング標準で働いてきました。その結果、機能が些細なものである場合、ほとんどの場合、構造化されていないスパゲッティになります.多くの中断と継続が邪魔になります.
単一の出口点 (他のすべての条件が同じ) により、コードが大幅に読みやすくなります。しかし、落とし穴があります: ポピュラーな建設
resulttype res;
if if if...
return res;
"res=" は "return" よりも優れているわけではありません。return ステートメントは 1 つですが、関数が実際に終了するポイントは複数あります。
複数の戻り値 (または "res="s) を持つ関数がある場合、単一の終了点を持ついくつかの小さな関数に分割することをお勧めします。
ガイドラインとして最後にシングルリターンに投票します。これは、一般的なコードのクリーンアップ処理に役立ちます...たとえば、次のコードを見てください...
void ProcessMyFile (char *szFileName)
{
FILE *fp = NULL;
char *pbyBuffer = NULL:
do {
fp = fopen (szFileName, "r");
if (NULL == fp) {
break;
}
pbyBuffer = malloc (__SOME__SIZE___);
if (NULL == pbyBuffer) {
break;
}
/*** Do some processing with file ***/
} while (0);
if (pbyBuffer) {
free (pbyBuffer);
}
if (fp) {
fclose (fp);
}
}
数回以上のリターンがある場合は、コードに何か問題がある可能性があります。それ以外の場合は、サブルーチン内の複数の場所から戻ることができると便利な場合があることに同意します。特に、コードがきれいになる場合はそうです。
sub Int_to_String( Int i ){
given( i ){
when 0 { return "zero" }
when 1 { return "one" }
when 2 { return "two" }
when 3 { return "three" }
when 4 { return "four" }
...
default { return undef }
}
}
このように書いた方が良いでしょう
@Int_to_String = qw{
zero
one
two
three
four
...
}
sub Int_to_String( Int i ){
return undef if i < 0;
return undef unless i < @Int_to_String.length;
return @Int_to_String[i]
}
これは簡単な例であることに注意してください
これはおそらく珍しい見方ですが、複数のreturnステートメントが優先されると信じている人は、4つのハードウェアブレークポイントのみをサポートするマイクロプロセッサでデバッガを使用する必要がなかったと思います。;-)
「矢印コード」の問題は完全に正しいですが、複数のreturnステートメントを使用すると解消されると思われる問題の1つは、デバッガーを使用している状況です。ブレークポイントを設定して、出口、つまり戻り条件が表示されることを保証するための便利なキャッチオール位置はありません。
関数内の return ステートメントが多いほど、その 1 つのメソッドの複雑さが増します。return ステートメントが多すぎるのではないかと考えている場合は、その関数のコード行が多すぎるかどうかを自問することをお勧めします。
しかし、そうではありません。1 つまたは複数の return ステートメントに問題はありません。一部の言語 (C++) では、他の言語 (C) よりも優れた方法です。
意見だけを書き留めてもよければ、それは私の意見です。
私は「単一の return ステートメント理論」に完全に完全に同意しません。コードの読みやすさ、ロジック、および記述面に関しては、ほとんど投機的であり、破壊的でさえあると思います。
単一のリターンを持つというその習慣は、より高レベルの抽象化 (関数型、組み合わせ型など) は言うまでもなく、むき出しの手続き型プログラミングでは不十分です。さらに、そのスタイルで書かれたすべてのコードが、複数のreturn ステートメントを持つように、特別な書き換えパーサーを通過することを望みます!
関数 (それが本当に関数/クエリである場合、「クエリとコマンドの分離」注記 - たとえば、Eiffel プログラミング言語を参照してください) は、それが持つ制御フロー シナリオと同じ数のリターン ポイントを定義する必要があります。それははるかに明確で、数学的に一貫しています。そしてそれは関数(つまりクエリ)を書く方法です
しかし、あなたのエージェントが実際に受信するミューテーション メッセージ (プロシージャ コール) については、それほど好戦的ではありません。
単一の戻り値の型を常に要求するのは意味がありません。何かを簡素化する必要があるかもしれないというフラグだと思います。複数のリターンが必要な場合もありますが、多くの場合、少なくとも1 つの出口ポイントを持つようにすることで、物事をよりシンプルに保つことができます。
複数の出口点を持つことは、基本的にGOTO
. それが悪いことかどうかは、猛禽類に対するあなたの気持ち次第です。
唯一の重要な質問は、「コードがより単純で、読みやすく、理解しやすくなる方法は?」ということです。複数のリターンがある方が簡単な場合は、それらを使用してください。
おそらく私は、「返品ステートメントは 1 つだけ」という大きな理由の 1 つを思い出すのに十分な年齢の数少ない人間の 1 人です。これは、コンパイラがより効率的なコードを発行できるようにするためです。関数呼び出しごとに、コンパイラは通常、いくつかのレジスタをスタックにプッシュして、それらの値を保持します。このようにして、関数はこれらのレジスタを一時記憶域に使用できます。関数が戻るとき、それらの保存されたレジスタをスタックからポップして、レジスタに戻す必要があります。これは、レジスタごとに 1 つの POP (または MOV -(SP),Rn) 命令です。多数の return ステートメントがある場合、それぞれがすべてのレジスターをポップする (コンパイルされたコードが大きくなる) か、コンパイラーが変更された可能性のあるレジスターを追跡し、それらのみをポップする (コード サイズを小さくする) 必要があります。ただし、コンパイル時間は増加します)。
今日でも 1 つの return ステートメントに固執することが理にかなっている理由の 1 つは、自動化されたリファクタリングの容易さです。IDE がメソッド抽出リファクタリング (行の範囲を選択してメソッドに変換する) をサポートしている場合、抽出したい行に return ステートメントが含まれている場合、特に値を返す場合、これを行うのは非常に困難です。 .
エラー処理が原因で、すでに暗黙的に複数の暗黙的な return ステートメントがあるため、それに対処してください。
ただし、プログラミングではよくあることですが、複数のリターンの実践に賛成する例と反対する例の両方があります。コードがより明確になる場合は、何らかの方法で実行してください。多くの制御構造を使用すると効果的です ( caseステートメントなど)。
優れた基準と業界のベストプラクティスのために、すべての関数に表示される正しい数のreturnステートメントを確立する必要があります。明らかに、1つのreturnステートメントを持つことに対してコンセンサスがあります。そこで、2に設定することを提案します。
皆さんが今すぐコードを調べて、出口点が1つしかない関数を見つけて、別の出口点を追加していただければ幸いです。どこでも構いません。
この変更の結果、間違いなくバグが減り、読みやすさが向上し、想像を絶する富が空から頭に浮かび上がります。
私は単一の return ステートメントを好みます。まだ指摘されていない理由の 1 つは、Eclipse JDT 抽出/インライン メソッドなど、一部のリファクタリング ツールが単一の終了点でより適切に機能することです。
パフォーマンス上の理由で必要になる場合があります (コンティニューと同じ必要性を持つ別の種類のキャッシュ ラインを取得したくない場合もあります)。
RAII を使用せずにリソース (メモリ、ファイル記述子、ロックなど) を割り当てた場合、複数のリターンによってエラーが発生しやすくなり、リリースを手動で複数回行う必要があり、注意深く追跡する必要があるため、確実に重複します。
例では:
function()
{
HRESULT error = S_OK;
if(SUCCEEDED(Operation1()))
{
if(SUCCEEDED(Operation2()))
{
if(SUCCEEDED(Operation3()))
{
if(SUCCEEDED(Operation4()))
{
}
else
{
error = OPERATION4FAILED;
}
}
else
{
error = OPERATION3FAILED;
}
}
else
{
error = OPERATION2FAILED;
}
}
else
{
error = OPERATION1FAILED;
}
return error;
}
私はそれを次のように書いたでしょう:
function() {
HRESULT error = OPERATION1FAILED;//assume failure
if(SUCCEEDED(Operation1())) {
error = OPERATION2FAILED;//assume failure
if(SUCCEEDED(Operation3())) {
error = OPERATION3FAILED;//assume failure
if(SUCCEEDED(Operation3())) {
error = OPERATION4FAILED; //assume failure
if(SUCCEEDED(Operation4())) {
error = S_OK;
}
}
}
}
return error;
}
どちらが確かに良いようです。
これは、手動リソース リリースの場合に特に役立ちます。どこで、どのリリースが必要かが非常に簡単だからです。次の例のように:
function() {
HRESULT error = OPERATION1FAILED;//assume failure
if(SUCCEEDED(Operation1())) {
//allocate resource for op2;
char* const p2 = new char[1024];
error = OPERATION2FAILED;//assume failure
if(SUCCEEDED(Operation2(p2))) {
//allocate resource for op3;
char* const p3 = new char[1024];
error = OPERATION3FAILED;//assume failure
if(SUCCEEDED(Operation3(p3))) {
error = OPERATION4FAILED; //assume failure
if(SUCCEEDED(Operation4(p2,p3))) {
error = S_OK;
}
}
//free resource for op3;
delete [] p3;
}
//free resource for op2;
delete [] p2;
}
return error;
}
このコードを RAII なしで (例外の問題を忘れて!) 複数の exit で記述すると、delete を複数回記述する必要があります。で書くと}else{
少し見苦しくなります。
しかし、RAII は複数の出口リソースの問題を意味のないものにします。
私は常に複数の return ステートメントを避けています。小さな機能でも。小さな関数は大きくなる可能性があり、複数のリターン パスを追跡すると、何が起こっているかを追跡することが (私の小さな心には) 難しくなります。単一のリターンにより、デバッグも容易になります。複数の return ステートメントの唯一の代替手段は、10 レベルの深さのネストされた IF ステートメントの乱雑な矢印であると投稿する人を見てきました。そのようなコーディングが行われることには確かに同意しますが、それが唯一の選択肢ではありません。複数の return ステートメントと IF のネストのどちらかを選択することはありません。リファクタリングして、両方を排除します。そして、それが私がコーディングする方法です。次のコードは両方の問題を解消し、非常に読みやすいと思います。
public string GetResult()
{
string rv = null;
bool okay = false;
okay = PerformTest(1);
if (okay)
{
okay = PerformTest(2);
}
if (okay)
{
okay = PerformTest(3);
}
if (okay)
{
okay = PerformTest(4);
};
if (okay)
{
okay = PerformTest(5);
}
if (okay)
{
rv = "All Tests Passed";
}
return rv;
}
エラーケース + 処理 + 戻り値をできるだけ近づけるために、複数の出口点を使用します。
そのため、真でなければならない条件 a、b、c をテストする必要があり、それぞれを異なる方法で処理する必要があります。
if (a is false) {
handle this situation (eg. report, log, message, etc.)
return some-err-code
}
if (b is false) {
handle this situation
return other-err-code
}
if (c is false) {
handle this situation
return yet-another-err-code
}
perform any action assured that a, b and c are ok.
a、b、および c は、a が入力パラメーター チェック、b が新しく割り当てられたメモリへのポインター チェック、c が「a」パラメーターの値のチェックなど、異なるものである可能性があります。
ネストされた IF の代わりに、do
/while(false)
を使用してどこでも抜け出す方法があります。
function()
{
HRESULT error = S_OK;
do
{
if(!SUCCEEDED(Operation1()))
{
error = OPERATION1FAILED;
break;
}
if(!SUCCEEDED(Operation2()))
{
error = OPERATION2FAILED;
break;
}
if(!SUCCEEDED(Operation3()))
{
error = OPERATION3FAILED;
break;
}
if(!SUCCEEDED(Operation4()))
{
error = OPERATION4FAILED;
break;
}
} while (false);
return error;
}
これにより、1 つの出口点が得られ、他の操作のネストが可能になりますが、実際の深い構造にはなりません。!SUCCEEDED が気に入らない場合は、いつでも FAILED を実行できます。この種のことにより、何も再インデントする必要なく、他の 2 つのチェックの間に他のコードを追加することもできます。
あなたが本当にクレイジーなら、そのif
ブロック全体もマクロ化できます。:D
#define BREAKIFFAILED(x,y) if (!SUCCEEDED((x))) { error = (Y); break; }
do
{
BREAKIFFAILED(Operation1(), OPERATION1FAILED)
BREAKIFFAILED(Operation2(), OPERATION2FAILED)
BREAKIFFAILED(Operation3(), OPERATION3FAILED)
BREAKIFFAILED(Operation4(), OPERATION4FAILED)
} while (false);
私はおそらくこれで嫌われるでしょうが、理想的にはreturnステートメントがまったくないはずです。関数は最後の式を返すだけで、完全に理想的なケースでは1つだけを含むべきです。
そうではない
function name(arg) {
if (arg.failure?)
return;
//code for non failure
}
むしろ
function name(arg) {
if (arg.failure?)
voidConstant
else {
//code for non failure
}
式ではない if ステートメントと return ステートメントは、私にとって非常に疑わしい慣行です。
returnステートメントを1つだけ達成するためにこれを行うことができます-最初に宣言し、最後に出力します-問題は解決しました:
$content = "";
$return = false;
if($content != "")
{
$return = true;
}
else
{
$return = false;
}
return $return;
さまざまな状況で、さまざまな方法が優れていると思います。たとえば、返される前に戻り値を処理する必要がある場合は、1 つの終了ポイントが必要です。しかし、他の状況では、複数のリターンを使用する方が快適です。
1 つのメモ。いくつかの状況で戻り値を処理する必要があるが、すべてではない場合、ProcessVal のようなメソッドを定義し、それを返す前に呼び出すための最良の解決策 (IMHO):
var retVal = new RetVal();
if(!someCondition)
return ProcessVal(retVal);
if(!anotherCondition)
return retVal;
私は通常、複数の return ステートメントを支持します。それらは読みやすいです。
良くない状況もあります。関数からの戻りが非常に複雑になる場合があります。すべての関数を複数の異なるライブラリにリンクする必要があった 1 つのケースを思い出します。1 つのライブラリは戻り値がエラー/ステータス コードであることを期待していましたが、他のライブラリはそうではありませんでした。単一の return ステートメントを使用すると、時間を節約できます。
誰も goto について言及していないことに驚いています。Goto は、誰もが信じているようなプログラミングの悩みの種ではありません。各関数に return が 1 つだけ必要な場合は、それを最後に置き、必要に応じて gotos を使用してその return ステートメントにジャンプします。フラグとアローのプログラミングは、見苦しくて実行速度が遅いため、絶対に避けてください。
私はこれに飛びつくことを知っていますが、私は真剣です。
return ステートメントは基本的に、手続き型プログラミング時代の名残です。それらは goto の形式であり、break、continue、if、switch/case、while、for、yield およびその他のいくつかのステートメントと、ほとんどの最新のプログラミング言語で同等のものです。
return ステートメントは、関数が呼び出されたポイントに効果的に「GOTO」し、そのスコープ内の変数を割り当てます。
return ステートメントは、私が「便利な悪夢」と呼んでいるものです。彼らは物事を素早く終わらせるように見えますが、将来的には大規模なメンテナンスの頭痛の種になります.
これは、オブジェクト指向プログラミングの最も重要で基本的な概念です。それがOOPの存在意義です。
メソッドから何かを返すときはいつでも、基本的にオブジェクトから状態情報を「漏えい」しています。状態が変化したかどうかは問題ではありません。また、この情報が他のオブジェクトから取得されたものであるかどうかも問題ではありません。呼び出し元には何の違いもありません。これが行うことは、オブジェクトの動作がオブジェクトの外にあることを可能にし、カプセル化を破ることです。これにより、呼び出し元は壊れやすい設計につながる方法でオブジェクトの操作を開始できます。
c2.com またはウィキペディアでデメテルの法則 (LoD) について読むことを開発者にお勧めします。LoD は、JPL のように文字通りの意味で「ミッション クリティカル」なソフトウェアの制約がある場所で使用されてきた設計哲学です。コード内のバグの量を減らし、柔軟性を向上させることが示されています。
犬の散歩に基づく優れたアナロジーがあります。犬を散歩させるとき、足を物理的につかんで、犬が歩くように動かしません。犬に歩くように命じると、犬は自分の足を世話します。このアナロジーでの return ステートメントは、犬が脚をつかむのに相当します。
これらのどれも return ステートメントを必要としないことに気付くでしょう。コンストラクターがリターンだと思うかもしれませんが、何かに取り組んでいます。実際には、戻り値はメモリ アロケータからのものです。コンストラクターは、メモリにあるものを設定するだけです。これは、その新しいオブジェクトのカプセル化に問題がない限り問題ありません。なぜなら、それを作成したので、それを完全に制御できるからです。他の誰もそれを壊すことはできません。
他のオブジェクトの属性にアクセスするのは当然です。ゲッターが出てきました(しかし、ゲッターが悪いことはすでにわかっていましたよね?)。セッターは問題ありませんが、コンストラクターを使用することをお勧めします。継承は悪いことです。別のクラスから継承する場合、そのクラスの変更はすべてあなたを壊す可能性があり、おそらくそうするでしょう。型スニッフィングは悪いです (はい - LoD は、Java/C++ スタイルの型ベースのディスパッチが正しくないことを意味します。型について尋ねることは、たとえ暗黙的にであっても、カプセル化を破っています。型はオブジェクトの暗黙的な属性です。インターフェイスは正しいことです)。
では、なぜこれがすべて問題なのですか?あなたの宇宙が私の宇宙と大きく異なっていない限り、あなたはコードのデバッグに多くの時間を費やしています。再利用しない予定のコードを書いているわけではありません。ソフトウェア要件が変化しており、内部 API/インターフェースの変更が発生しています。return ステートメントを使用するたびに、非常にトリッキーな依存関係が導入されます。何かを返すメソッドは、返されるものがどのように使用されるかを知る必要があります。これは、すべてのケースです! インターフェースが変更されるとすぐに、一方の側または他方の側ですべてが壊れる可能性があり、長くて退屈なバグハントに直面することになります.
それらは実際にはコード内の悪性のガンです。なぜなら、それらを使い始めると、他の場所でさらに使用されるようになるからです (これが、オブジェクト システム間でメソッド チェーンを返すことがよくある理由です)。
では、代替手段は何ですか?
OOP を使用すると、他のオブジェクトに何をすべきかを伝え、それを処理させることが目標です。そのため、物事を行う手続き的な方法を忘れる必要があります。本当に簡単です - return ステートメントを決して書かないでください。同じことを行うためのより良い方法があります。
本当に返信が必要な場合は、コールバックを使用してください。埋められるデータ構造を渡します。そうすれば、インターフェイスをクリーンでオープンに保ち、システム全体の脆弱性を軽減し、適応性を高めることができます。テールコールの最適化と同じように、システムを遅くすることはありません。実際、システムを高速化できます。ただし、この場合はテールコールがないため、スタックを操作する時間を無駄にする必要さえありません。戻り値。
これらの引数に従えば、return ステートメントはまったく必要ないことがわかります。
これらのプラクティスに従えば、バグ探しに費やす時間が大幅に短縮され、要件の変更にはるかに迅速に適応し、独自のコードを理解する際の問題が少なくなることにすぐに気付くでしょう。
上手に使いこなせば複数回出口がいい
最初のステップは、終了の理由を特定することです。
1.関数を実行する必要がない
2.エラーが見つかった
3.早期に完了した
4.正常に完了
した 「1.関数を実行する必要がない」を「3.早期に完了」にグループ化できると思います(もしそうなら、非常に早い完了)。
2 番目のステップは、関数の外の世界に終了の理由を知らせることです。擬似コードは次のようになります。
function foo (input, output, exit_status)
exit_status == UNDEFINED
if (check_the_need_to_execute == false) then
exit_status = NO_NEED_TO_EXECUTE // reason #1
exit
useful_work
if (error_is_found == true) then
exit_status = ERROR // reason #2
exit
if (need_to_go_further == false) then
exit_status = EARLY_COMPLETION // reason #3
exit
more_work
if (error_is_found == true) then
exit_status = ERROR
else
exit_status = NORMAL_COMPLETION // reason #4
end function
明らかに、上の図の作業の塊を別の関数に移動することが有益である場合は、そうする必要があります。
必要に応じて、終了ステータスをより具体的に示すことができます。たとえば、いくつかのエラー コードと早期完了コードを使用して、終了の理由 (または場所) を特定できます。
この関数を単一の出口しか持たない関数に強制したとしても、とにかく終了ステータスを指定する必要があると思います。呼び出し元は、出力を使用してもよいかどうかを知る必要があり、メンテナンスに役立ちます。