関数内にreturnステートメントを1つだけ持つ方が良い方法である理由はありますか?
それとも論理的に正しいとすぐに関数から戻ることは問題ありませんか?つまり、関数には多くのreturnステートメントがある可能性があります。
関数内にreturnステートメントを1つだけ持つ方が良い方法である理由はありますか?
それとも論理的に正しいとすぐに関数から戻ることは問題ありませんか?つまり、関数には多くのreturnステートメントがある可能性があります。
回答:
メソッドの最初に、「簡単な」状況に戻るためのステートメントがいくつかあります。たとえば、これ:
public void DoStuff(Foo foo)
{
if (foo != null)
{
...
}
}
...このように読みやすくすることができます(IMHO):
public void DoStuff(Foo foo)
{
if (foo == null) return;
...
}
だから、はい、私は関数/メソッドから複数の「出口点」を持つことは問題ないと思います。
DoStuff() { DoStuffInner(); IncreaseStuffCallCounter(); }
コードコンプリートについて誰も言及も引用もしていないので、私がやります。
各ルーチンのリターン数を最小限に抑えます。下部でそれを読んで、あなたがそれが上のどこかに戻った可能性に気づいていないなら、ルーチンを理解することはより困難です。
読みやすさを向上させる場合は、リターンを使用します。特定のルーチンでは、答えがわかったら、すぐにそれを呼び出し元のルーチンに戻したいことがあります。ルーチンがクリーンアップを必要としない方法で定義されている場合、すぐに戻らないことは、さらにコードを記述する必要があることを意味します。
私は技術が実際に有用であることがわかってきたように、複数の出口点に対して任意に決定することが非常に賢明だろうと言うでしょう何度も何度も、実際に私は頻繁にしている、既存のコードをリファクタリングし、明確にするための複数の出口点に。したがって、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条件と中括弧が適切に一致している。
この方法では、コードのメンテナンスが非常に困難になり、エラーが発生しやすくなります。
構造化プログラミングによると、関数ごとに1つのreturnステートメントのみを使用する必要があります。これは複雑さを制限するためです。Martin Fowlerのような多くの人々は、複数のreturnステートメントで関数を書く方が簡単だと主張しています。彼は彼が書いた古典的なリファクタリングの本でこの議論を提示します。これは、彼の他のアドバイスに従い、小さな関数を書く場合にうまく機能します。私はこの見解に同意し、厳密な構造化プログラミングの純粋主義者だけが関数ごとに単一のreturnステートメントを遵守します。
GOTO
として、機能が存在する場合でも制御フローを移動するために使用することについて話しました。「決して使用しない」とは表示されませんGOTO
。
Kent Beckが、ルーチンを作成する実装パターンでガード句について議論するときに注意するように、単一の入口と出口点があります...
「同じルーチン内の多くの場所に出入りするときに起こり得る混乱を防ぐためでした。実行されたステートメントを理解するのが難しい作業である多くのグローバルデータで記述されたFORTRANまたはアセンブリ言語プログラムに適用される場合、それは理にかなっています。 。小さなメソッドとほとんどローカルデータで、それは不必要に保守的です。」
ガード節で記述された関数は、1つの長いネストされたif then else
ステートメントの束よりもはるかに簡単に理解できます。
副作用のない関数では、1つ以上の戻り値を持つ理由はありません。関数スタイルで記述する必要があります。副作用のあるメソッドでは、物事はより順次的(時間インデックス付き)であるため、returnステートメントを実行を停止するコマンドとして使用して、命令スタイルで記述します。
つまり、可能であれば、このスタイルを優先します
return a > 0 ?
positively(a):
negatively(a);
これ以上
if (a > 0)
return positively(a);
else
return negatively(a);
ネストされた条件の複数のレイヤーを作成している場合は、たとえば述語リストを使用して、それをリファクタリングできる方法がおそらくあります。ifsとelsesが構文的に大きく離れている場合は、それをより小さな関数に分解することをお勧めします。1画面以上のテキストにまたがる条件付きブロックは読みにくいです。
すべての言語に適用される厳格な規則はありません。単一のreturnステートメントがあるようなものは、コードを良くしません。しかし、優れたコードでは、そのように関数を記述できる傾向があります。
私はそれをC ++のコーディング標準で見たことがあります。これはCからの二日酔いでした。RAIIや他の自動メモリ管理がない場合は、リターンごとにクリーンアップする必要があります。つまり、カットアンドペーストのいずれかです。クリーンアップまたはgoto(管理された言語では「最終的に」と論理的に同じ)のどちらも、不適切な形式と見なされます。C ++や他の自動メモリシステムでスマートポインターやコレクションを使用することを実践している場合は、その強い理由はなく、読みやすさや判断の呼び出しがすべてになります。
auto_ptr
プレーンポインタを並行して使用できます。そもそも、最適化されていないコンパイラで「最適化された」コードを書くのは奇妙です。
try
... などfinally
)が含まれておらず、リソースのメンテナンスを行う必要がある場合は、単一のメソッドの最後に戻ります。これを行う前に、コードをリファクタリングして状況を解消することを真剣に検討する必要があります。
関数の途中の returnステートメントは悪いという考えに頼っています。returnを使用して、関数の上部にいくつかのガード句を構築し、もちろん関数の最後に何が問題なく戻るかをコンパイラーに指示できますが、関数の途中での戻りは見落としやすく、関数を解釈しにくくします。
関数内にreturnステートメントを1つだけ持つ方が良い方法である理由はありますか?
はい、あります:
多くの場合、この質問は、複数のリターンの間の誤った二分法または深くネストされたifステートメントとして提起されます。ほとんどの場合、出口点が1つだけの非常に線形な(深い入れ子ではない)3番目のソリューションがあります。
更新:MISRAガイドラインは、シングルエグジットも促進しているようです。
明確にするために、私は複数のリターンを持つことは常に間違っていると言っているのではありません。しかし、それ以外の点では同等のソリューションを考えると、単一のリターンを持つソリューションを好む多くの正当な理由があります。
Contract.Ensures
。これは、複数のリターンポイントで引き続き使用できるためです。
goto
共通のクリーンアップコードを取得するために使用している場合、クリーンアップコードreturn
の最後にシングルが存在するように関数を単純化した可能性があります。したがって、問題はで解決したと言えるかもしれませんがgoto
、単純化して1つに解決したと思いますreturn
。
関数の最後に単一のブレークポイントを設定して、実際に返される値を確認できるため、単一の出口点があるとデバッグに利点があります。
一般に、関数からの出口点は1つだけにしようとしています。ただし、そうすることで、実際には必要以上に複雑な関数本体が作成される場合があります。その場合は、複数の出口点を用意することをお勧めします。結果として生じる複雑さに基づく「判断の呼びかけ」である必要がありますが、目標は、複雑さと理解しやすさを犠牲にすることなく、できるだけ少ない出口点でなければなりません。
いいえ、1970年代にはもう住んでいないので。関数が長すぎて複数回の戻りが問題になる場合は、長すぎます。
(例外のある言語の複数行関数には、とにかく複数の出口点があるという事実とはかなり異なります。)
それが本当に複雑でない限り、私の好みは単一の出口です。場合によっては、複数の存在ポイントが他のより重要な設計問題を隠す可能性があることを発見しました:
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つの問題は、「名前付き戻り値の最適化」(別名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の名前付き戻り値の最適化」を参照してください。
throw new ArgumentNullException()
、この場合のC#の場合のように、戻り点でもあります)、私は他の考慮事項を本当に気に入りました、それらはすべて私にとって有効であり、いくつかにおいて重要である可能性がありますニッチなコンテキスト。
foo
テストされては何をするかどうかである主題とは何の関係も、ありませんif (foo == NULL) return; dowork;
かif (foo != NULL) { dowork; }
単一の出口点があると、循環的複雑度が低くなるため、理論的には、コードを変更したときにコードにバグが発生する可能性が低くなります。ただし、実践では、より実用的なアプローチが必要であると示唆する傾向があります。したがって、私は単一の出口点を持つことを目指しがちですが、より読みやすい場合は、コードにいくつかの出口点を持たせることができます。
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);
}
確かに、それは長くて少し厄介ですが、この方法で関数をリファクタリングする過程で、
通常、複数の戻り値は(C#で作成するコードでは)良いと思います。シングルリターンスタイルはCからの引き継ぎですが、おそらくCでコーディングしていません。
すべてのプログラミング言語で、メソッドに1つの出口点のみを要求する法律はありません。一部の人々はこのスタイルの優位性を主張し、時にはそれを「ルール」または「法則」に引き上げますが、この信念はいかなる証拠や研究によっても裏付けられていません。
リソースを明示的に割り当て解除する必要があるCコードでは、複数の戻りスタイルが悪い習慣である可能性がありますが、Java、C#、Python、JavaScriptなどの言語で、自動ガベージコレクションやtry..finally
ブロック(およびusing
C#のブロック) )、そしてこの議論は適用されません-これらの言語では、集中的な手動のリソース割り当て解除を必要とすることは非常にまれです。
単一の戻り値が読みやすい場合とそうでない場合があります。コードの行数を減らしたり、ロジックをより明確にしたり、中括弧やインデントや一時変数の数を減らしたりしていないか確認してください。
したがって、これはレイアウトや読みやすさの問題であり、技術的な問題ではないため、芸術的な感性に合わせてできるだけ多くのリターンを使用してください。
結果として生じる不可避の「矢印」プログラミングについて言うのが悪いのと同じように、単一の出口点を持つことについて言うのは良いことです。
入力検証またはリソース割り当て中に複数の出口点を使用する場合、すべての「エラー終了」を目に見える形で関数の上部に配置しようとします。
「SSDSLPedia」のスパルタンプログラミングの記事と「Portland Pattern RepositoryのWiki」の単一機能の出口点の記事の両方に、これに関する洞察に満ちた議論があります。また、もちろん、この投稿を検討する必要があります。
たとえば、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を個別にテストすることが非常に簡単になります。
明らかに、オブジェクト指向で例外対応の言語では、そのようなifステートメントは使用せず(または、十分な努力を払わずにそれを回避できれば)、コードははるかにわかりやすくなります。そして非矢じり。そして、非最終的なリターンのほとんどはおそらく例外です。
要するに;
格言- 美しさは見る人の目にあります。
NetBeansやIntelliJ IDEA、PythonやPHPで誓う人もいます。
一部のショップでは、これを主張すると仕事を失う可能性があります。
public void hello()
{
if (....)
{
....
}
}
問題は、すべての可視性と保守性です。
論理代数と状態機械の使用を減らして単純化するためにブール代数を使うことに夢中です。ただし、私の目には「数学的手法」を使用してコーディングすることは不適切であると考えていた過去の同僚もいました。そしてそれは悪い習慣になるでしょう。申し訳ありませんが、私が採用している手法は非常にわかりやすく、保守しやすくなっています。6か月後にコードに戻ると、コードをはっきりと理解しているので、ことわざのスパゲッティが散らかっています。
(元クライアントが言っていたように)ちょっとバディは、私が修正する必要があるときにそれを修正する方法を知っている限り、あなたがやりたいことをします。
20年前のことを覚えています。私の同僚は、今日アジャイル開発戦略と呼ばれるものを採用したことで解雇されました。彼は細心の段階的な計画を立てていました。しかし彼のマネージャーは彼に怒鳴っていました。「ユーザーに機能を段階的にリリースすることはできません。ウォーターフォールに固執する必要があります。」マネージャーへの彼の反応は、段階的な開発は顧客のニーズにより正確になるだろうというものでした。彼は顧客のニーズに合わせて開発することを信じていましたが、マネージャーは「顧客の要件」に合わせたコーディングを信じていました。
私たちは、データの正規化、MVP、MVCの境界を破ったことでしばしば罪を犯します。関数を作成する代わりにインライン化します。私たちは近道をとります。
個人的には、PHPは悪い習慣だと思いますが、何を知っていますか。すべての理論的な議論は、1つのルールセットを実行しようとすることになります。
品質=精度、保守性、収益性。
他のすべてのルールはバックグラウンドにフェードインします。そしてもちろん、このルールは決して衰えません:
怠惰は、優れたプログラマの美徳です。
私は、ガード節を使用して早期に戻り、そうでなければメソッドの最後で終了することに傾倒しています。単一の入り口と出口のルールには歴史的な重要性があり、複数の戻り値(および多くの欠陥)を持つ単一のC ++メソッドで10 A4ページまで実行されたレガシーコードを処理するときに特に役立ちました。最近では、複数の出口を理解するためのインピーダンスが少なくなるようにメソッドを小さく保つことが、承認された優れた方法です。上記からコピーした次のKronozの例では、問題は// Rest of code ...?
void string fooBar(string s, int? i) {
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;
}
この例は多少不自然ですが、foreachループをLINQステートメントにリファクタリングして、ガード節と見なしたくなるかもしれません。繰り返しになりますが、不自然な例では、コードの目的は明らかではなく、someFunction()には他の副作用があるか、結果が//コードの残りの部分...で使用されている可能性があります。
if (string.IsNullOrEmpty(s) || i == null) return null;
if (someFunction(s, i).Any(r => !r.Passed)) return null;
次のリファクタリングされた関数を与える:
void string fooBar(string s, int? i) {
if (string.IsNullOrEmpty(s) || i == null) return null;
if (someFunction(s, i).Any(r => !r.Passed)) return null;
// Rest of code...
return ret;
}
null
では、引数が受け入れられないことを示す例外をスローする代わりに、なぜ戻るのでしょうか。
私が考えることができる1つの正当な理由は、コードの保守のためです。出口が1つしかないためです。結果の形式を変更したい場合は、実装するのがはるかに簡単です。また、デバッグのために、そこにブレークポイントを貼り付けることができます:)
そうは言っても、私はかつてコーディング標準が「関数ごとに1つのreturnステートメント」を課したライブラリーで作業する必要があり、それはかなり難しいと思いました。私はたくさんの数値計算コードを書いていて、しばしば「特別な場合」があるので、コードは結局追跡するのが非常に困難になりました...
私はあなたに単一の出口パスを強制する恐ろしいコーディング標準を使用してきました、そしてその機能が些細なものでない場合、結果はほとんど常に非構造化スパゲッティです-あなたは多くのブレークに行き着き、邪魔になります。
if
成功を返すかどうかを返す各メソッド呼び出しの前にあるステートメントをスキップするよう心がける必要があります:(
私の通常のポリシーは、コードを追加してコードの複雑さを大幅に軽減しない限り、関数の最後にreturnステートメントを1つだけ置くことです。実際、私はむしろEiffelのファンです。Eiffelは、returnステートメントがないことによって唯一のreturnルールを適用します(結果を入れるために自動作成された「result」変数があります)。
確かに、複数のリターンでコードを明確にできる場合があり、それらがない場合の明らかなバージョンよりも明確になります。複数のreturnステートメントなしでは理解できないほど複雑な関数がある場合は、より多くの再作業が必要であると主張することができますが、そのようなことについて実際的であると良い場合があります。
最終的にいくつかのリターンを超える場合は、コードに問題がある可能性があります。それ以外の場合、特にコードをよりクリーンにするときに、サブルーチンの複数の場所から戻ることができると便利な場合があることに同意します。
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]
}
これはほんの一例です
ガイドラインとして、最後にシングルリターンに投票します。これは、一般的なコードのクリーンアップ処理に役立ちます...たとえば、次のコードを見てください...
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);
}
}
これはおそらく珍しい見方ですが、複数のreturnステートメントが支持されると信じている人は、4つのハードウェアブレークポイントのみをサポートするマイクロプロセッサでデバッガを使用する必要がなかったと思います。;-)
「矢印コード」の問題は完全に正しいですが、複数のreturnステートメントを使用するときに解消すると思われる問題の1つは、デバッガーを使用している状況です。ブレークポイントを設定して、出口、つまり戻り条件が確実に表示されるようにするための便利なキャッチオールポジションはありません。