web-dev-qa-db-ja.com

ネストを減らすために「if」ステートメントを反転する

コードで ReSharper を実行したとき、たとえば:

    if (some condition)
    {
        Some code...            
    }

ReSharperは上記の警告(「if」ステートメントを反転してネストを減らす)を提供し、次の修正を提案しました。

   if (!some condition) return;
   Some code...

なぜそれが良いのかを理解したいと思います。私はいつも、「goto」のように問題のあるメソッドの途中で「return」を使用すると考えていました。

250
Lea Cohen

メソッドの途中で戻ることは必ずしも悪いことではありません。コードの意図がより明確になった場合は、すぐに戻った方が良いかもしれません。例えば:

double getPayAmount() {
    double result;
    if (_isDead) result = deadAmount();
    else {
        if (_isSeparated) result = separatedAmount();
        else {
            if (_isRetired) result = retiredAmount();
            else result = normalPayAmount();
        };
    }
     return result;
};

この場合、_isDeadがtrueであれば、すぐにメソッドから抜け出すことができます。代わりにこのように構成する方が良い場合があります。

double getPayAmount() {
    if (_isDead)      return deadAmount();
    if (_isSeparated) return separatedAmount();
    if (_isRetired)   return retiredAmount();

    return normalPayAmount();
};   

リファクタリングカタログ からこのコードを選択しました。この特定のリファクタリングは次のように呼ばれます。ネストされた条件付きをガード句に置き換えます。

274
jop

見た目だけでなく、メソッド内の 最大ネストレベル も削減します。これは、メソッドを理解しやすくするため、一般的にプラスと見なされます(そして実際、 manystaticanalysistools コード品質の指標の1つとして、この指標を提供してください)。

一方で、メソッドに複数の出口点を持たせることもできます。これは、別のグループがノーと考えるものです。

個人的には、ReSharperと最初のグループに同意します(例外がある言語では、「複数の出口点」について議論するのはばかげていると思います。ほとんど何でもスローできるため、すべてのメソッドに多数の潜在的な出口点があります)。

パフォーマンスについて:両方のバージョンshouldは同等です(ILレベルではない場合は、ジッターはすべての言語でコードを通過します)。理論的にはこれはコンパイラに依存しますが、実際に広く使用されている今日のコンパイラは、これよりもはるかに高度なコード最適化のケースを処理できます。

326
Jon

これは少し宗教的な議論ですが、ネスティングを少なくする方がいいとReSharperに同意します。これは、関数からの複数のリターンパスを持つことのマイナスを上回ると信じています。

ネストが少ない主な理由は、コードの可読性と保守性を向上させることです。他の多くの開発者は将来コードを読む必要があり、インデントの少ないコードは一般的に読みやすいことを覚えておいてください。

前提条件は、関数の開始時に早く戻ることができる場所の良い例です。前提条件チェックの存在によって、関数の残りの部分の可読性が影響を受けるのはなぜですか?

メソッドから複数回返すことに関するマイナス面については、デバッガーは非常に強力になり、特定の関数がどこでいつ返ってくるかを正確に見つけるのは非常に簡単です。

関数に複数の戻り値を持たせることは、保守プログラマの仕事に影響を与えません。

コードの可読性が低下します。

101

他の人が述べたように、パフォーマンスに影響はありませんが、他の考慮事項があります。これらの有効な懸念は別として、これは状況によっては落とし穴につながる可能性もあります。代わりにdoubleを扱っていたとします:

public void myfunction(double exampleParam){
    if(exampleParam > 0){
        //Body will *not* be executed if Double.IsNan(exampleParam)
    }
}

一見同等の反転とは対照的です:

public void myfunction(double exampleParam){
    if(exampleParam <= 0)
        return;
    //Body *will* be executed if Double.IsNan(exampleParam)
}

そのため、特定の状況では、正しく反転したifであると思われるものはそうではない場合があります。

68
Michael McGowan

関数の最後でのみ戻るという考えは、言語が例外をサポートする前の時代から戻ってきました。これにより、プログラムはメソッドの最後にクリーンアップコードを置くことができるようになり、それが呼び出され、クリーンアップコードがスキップされる原因となったメソッドの戻り値を非表示にしないプログラマーがいることを確認できました。 。クリーンアップコードをスキップすると、メモリまたはリソースリークが発生する可能性があります。

ただし、例外をサポートする言語では、そのような保証はありません。例外をサポートする言語では、ステートメントまたは式を実行すると、メソッドを終了させる制御フローが発生する可能性があります。これは、finallyまたはusingキーワードを使用してクリーンアップを実行する必要があることを意味します。

とにかく、多くの人が「メソッドの最後でのみ戻る」ガイドラインを引用する理由を理解せずに引用していると思います。読みやすさを改善するためにネストを減らすことはおそらくより良い目的です。

50
Scott Langham

これらの逆向きのifの名前があることを付け加えます-Guard Clause。できる限り使用します。

冒頭に2つのコード画面があり、他にない場合にコードを読むのは嫌です。 ifとreturnを反転するだけです。そうすれば、誰もスクロールに時間を浪費しません。

http://c2.com/cgi/wiki?GuardClause

27
Piotr Perak

美観に影響するだけでなく、コードのネストも防ぎます。

実際には、データが有効であることを保証するための前提条件として機能することもできます。

21
Rion Williams

これはもちろん主観的ですが、次の2つの点で大幅に改善されると思います。

  • conditionが成り立つ場合、関数には何もすることがないことがすぐにわかります。
  • ネストのレベルを下げます。ネストは、想像以上に読みやすさを損ないます。
18
Deestan

複数のリターンポイントは、各リターンポイントの前にクリーンアップコードを複製することを余儀なくされたため、C(および程度は低いがC++)では問題でした。ガベージコレクションでは、try | finallyコンストラクトとusingブロック。これらを恐れる必要はありません。

最終的には、あなたと同僚が読みやすいと思うものに帰着します。

14
Richard Poole

パフォーマンスに関しては、2つのアプローチの間に顕著な違いはありません。

しかし、コーディングはパフォーマンス以上のものです。明瞭さと保守性も非常に重要です。そして、パフォーマンスに影響を与えないこのような場合、重要なのはそれだけです。

どちらのアプローチが望ましいかについては、競合する考え方があります。

1つのビューは、他の人が言及したものです。2番目のアプローチは、ネストレベルを減らし、コードの明快さを向上させます。これは命令型のスタイルでは自然です。何もすることがない場合は、早めに戻ることもできます。

より機能的なスタイルの観点からのもう1つのビューは、メソッドには1つの出口ポイントのみが必要であることです。関数型言語のすべては表現です。したがって、if文には常にelse句が必要です。それ以外の場合、if式は常に値を持つとは限りません。したがって、機能的なスタイルでは、最初のアプローチはより自然です。

12
Jeffrey Sax

ガード句または前提条件(おそらくおわかりのように)は、特定の条件が満たされているかどうかを確認し、プログラムのフローを中断します。 ifステートメントの1つの結果だけに本当に興味がある場所に最適です。だから言うよりも:

if (something) {
    // a lot of indented code
}

条件を逆にし、その逆の条件が満たされた場合に中断します

if (!something) return false; // or another value to show your other code the function did not execute

// all the code from before, save a lot of tabs

returnは、gotoほど汚くない。関数を実行できなかった残りのコードを示す値を渡すことができます。

ネストされた条件でこれを適用できる最良の例が表示されます。

if (something) {
    do-something();
    if (something-else) {
        do-another-thing();
    } else {
        do-something-else();
    }
}

対:

if (!something) return;
do-something();

if (!something-else) return do-something-else();
do-another-thing();

最初のものはよりクリーンだと主張する人はほとんどいませんが、もちろん、それは完全に主観的です。一部のプログラマーは、インデントによって何かが動作している条件を知りたがりますが、メソッドフローは線形に保ちたいと考えています。

プリコンがあなたの人生を変えたり、あなたを置いたりすることを一瞬示唆するつもりはありませんが、コードをほんの少し読みやすくするかもしれません。

11
Oli

ここにはいくつかの良い点がありますが、メソッドが非常に長い場合、複数の戻り点読み取り不可能な場合がありますもあります。つまり、複数のリターンポイントを使用する場合は、メソッドが短いことを確認してください。そうしないと、複数のリターンポイントの可読性ボーナスが失われる可能性があります。

9
Jon Limjap

パフォーマンスは2つの部分に分かれています。ソフトウェアが実稼働しているときはパフォーマンスがありますが、開発およびデバッグ中にもパフォーマンスが必要です。開発者が最後に望むことは、些細なことを「待つ」ことです。最終的に、最適化を有効にしてこれをコンパイルすると、同様のコードが生成されます。したがって、両方のシナリオで成果を上げるこれらの小さなトリックを知っておくとよいでしょう。

質問のケースは明確で、ReSharperは正しいです。 ifステートメントをネストしてコード内に新しいスコープを作成するのではなく、メソッドの開始時に明確なルールを設定しています。読みやすさが向上し、保守が容易になり、行きたい場所を見つけるためにふるいにかける必要があるルールの量が減ります。

8
Ryan Ternier

個人的には、1つの出口ポイントのみを好みます。メソッドを短く、要点を保つと簡単に達成でき、コードで作業する次の人に予測可能なパターンを提供します。

例えば。

 bool PerformDefaultOperation()
 {
      bool succeeded = false;

      DataStructure defaultParameters;
      if ((defaultParameters = this.GetApplicationDefaults()) != null)
      {
           succeeded = this.DoSomething(defaultParameters);
      }

      return succeeded;
 }

これは、終了する前に関数内の特定のローカル変数の値を確認するだけの場合にも非常に便利です。あなたがする必要があるのは、最終的なリターンにブレークポイントを置くことであり、例外がスローされない限り、必ずヒットすることが保証されています。

7
ilitirit

コードがどのように見えるかに関する多くの正当な理由。しかし、結果はどうですか?

いくつかのC#コードとそのILコンパイル済みフォームを見てみましょう。


using System;

public class Test {
    public static void Main(string[] args) {
        if (args.Length == 0) return;
        if ((args.Length+2)/3 == 5) return;
        Console.WriteLine("hey!!!");
    }
}

この単純なスニペットはコンパイルできます。生成された.exeファイルをildasmで開き、結果を確認できます。すべてのアセンブラーのことを投稿するわけではありませんが、結果を説明します。

生成されたILコードは次のことを行います。

  1. 最初の条件がfalseの場合、2番目の条件があるコードにジャンプします。
  2. 真の場合、最後の命令にジャンプします。 (注:最後の命令はリターンです)。
  3. 2番目の条件では、結果が計算された後に同じことが起こります。比較して:falseの場合はConsole.WriteLineに到達し、これがtrueの場合は終了します。
  4. メッセージを印刷して戻ります。

そのため、コードは最後までジャンプするようです。ネストされたコードで通常のifを行うとどうなりますか?

using System;

public class Test {
    public static void Main(string[] args) {
        if (args.Length != 0 && (args.Length+2)/3 != 5) 
        {
            Console.WriteLine("hey!!!");
        }
    }
}

結果は、IL命令で非常に似ています。違いは、条件ごとにジャンプする前であることです。falseの場合は次のコードに進み、trueの場合は終了します。そして今、ILコードの流れが良くなり、3つのジャンプがあります(コンパイラーはこれを少し最適化しました)。 2. 2番目:1つの命令を回避するための2番目の条件の中間。 3. 3番目:2番目の条件がfalseの場合、最後にジャンプします。

とにかく、プログラムカウンターは常にジャンプします。

5
graffic

それは単に議論の余地がある。早期復帰の問題に関する「プログラマー間の合意」はありません。私の知る限り、それは常に主観的です。

パフォーマンスの引数を作成することは可能です。なぜなら、ほとんどの場合に当てはまるように記述された条件を用意する方がよいからです。また、より明確であると主張することもできます。一方、ネストされたテストを作成します。

この質問に対して最終的な答えが得られるとは思わない。

4
unwind

理論的には、ifを反転すると、分岐予測のヒット率が向上するとパフォーマンスが向上する可能性があります。実際には、特にコンパイル後に分岐予測がどのように動作するかを正確に知ることは非常に難しいと思うので、アセンブリコードを書いている場合を除き、日々の開発ではそれを行いません。

分岐予測の詳細 こちら

4
jfg956

既に多くの洞察に満ちた答えがありますが、それでも少し異なる状況に向けます。前提条件の代わりに、実際に関数の上に置く必要があります。ステップバイステップの初期化を考えてください。各ステップが成功することを確認してから、次のステップに進む必要があります。この場合、上部のすべてをチェックすることはできません。

SteinbergのASIOSDKを使用してASIOホストアプリケーションを作成すると、ネストパラダイムに従っているため、コードが実際には読み取れませんでした。それは8レベルの深さのようになり、上記のAndrew Bullockが述べたように、そこに設計上の欠陥はありません。もちろん、いくつかの内部コードを別の関数にパックし、残りのレベルをネストして読みやすくすることもできますが、これはかなりランダムに思えます。

ネストをガード句に置き換えることで、クリーンアップコードの一部に関する誤解を発見しました。クリーンアップコードは、関数の最後ではなく、はるかに早く発生するはずでした。ネストされたブランチでは、私はそれを見たことがなかったでしょう、あなたはそれらが私の誤解につながったと言うことさえできます。

したがって、これは、逆ifsがより明確なコードに貢献できる別の状況かもしれません。

3

複数の出口点を避けるcanはパフォーマンスの向上につながります。 C#についてはわかりませんが、C++では、名前付き戻り値の最適化(Copy Elision、ISO C++ '03 12.8/15)は単一の出口点を持つことに依存しています。この最適化により、コピーが戻り値を構築することを回避します(特定の例では重要ではありません)。これにより、関数が呼び出されるたびにコンストラクターとデストラクターを保存するため、タイトなループでパフォーマンスが大幅に向上する可能性があります。

しかし、追加のコンストラクターとデストラクターの呼び出しを保存するケースの99%で、ネストされたifブロックが読みやすさを失うことは価値がありません(他の人が指摘しているように)。

3
shibumi

私の意見では、void(またはチェックすることのない無駄なリターンコード)を返すだけであれば早期復帰は問題なく、ネストを回避すると同時に機能が実行されたことを明示するため、読みやすさが向上する可能性があります.

実際にreturnValueを返している場合-ネストは通常​​、1つの場所(最後-duh)でreturnValueを返すためのより良い方法であり、多くの場合、コードのメンテナンス性が向上する可能性があります。

2
JohnIdol

それは意見の問題です。

私の通常のアプローチは、単一行のifsを避け、メソッドの途中で戻ることです。

メソッドの至る所に示されているような行は必要ありませんが、メソッドの最上部にある一連の前提条件をチェックし、すべてが合格した場合にのみ実際の作業を行うことについて、何か言いたいことがあります。

2
Colin Pickard

確かではありませんが、R#は大きなジャンプを避けようとしていると思います。 IF-ELSEがある場合、コンパイラは次のようなことを行います。

条件false-> false_condition_labelへのジャンプ

true_condition_label:instruction1 ... instruction_n

false_condition_label:instruction1 ... instruction_n

エンドブロック

条件がtrueの場合、ジャンプもロールアウトL1キャッシュもありませんが、false_condition_labelへのジャンプは非常に遠くなる可能性があり、プロセッサは自分のキャッシュをロールアウトする必要があります。キャッシュの同期は高価です。 R#は、ファージャンプをショートジャンプに置き換えようとします。この場合、すべての命令が既にキャッシュにある可能性が高くなります。

前述のように、一般的な合意はありません。迷惑を軽減するために、この種の警告を「ヒント」に減らすことができます

0
Bluenuance

私の考えは、「関数の途中で」戻ることはそれほど「主観的」であってはならないということです。理由は非常に簡単です。次のコードを使用してください。

 function do_something(data){
 
 if(!is_valid_data(data))
 return false; 
 
 
 do_something_that_take_an_hour(data); 
 
 istance = new object_with_very_painful_constructor(data); 
 
 if(istance is not valid){
 error_message( ); 
 return; 
 
} 
 connect_to_database(); 
 get_some_other_data(); 
 return; 
 } 

おそらく最初の「戻り」は_SO直感的ではありませんが、それは本当に節約になります。きれいなコードに関する「アイデア」が多すぎるため、「主観的な」悪いアイデアを失うには、より多くの練習が必要です。

0

この種のコーディングにはいくつかの利点がありますが、私にとって大きな勝利は、すぐに戻ることができれば、アプリケーションの速度を改善できることです。 IE前提条件Xのために、エラーですばやく戻ることができることを知っています。これにより、最初にエラーが解消され、コードの複雑さが軽減されます。多くの場合、CPUパイプラインはよりクリーンにできるため、パイプラインのクラッシュまたは切り替えを停止できます。第二に、ループ状態にある場合、すばやくブレークまたは復帰することで、CPUを大幅に節約できます。一部のプログラマーはループ不変式を使用してこの種の迅速な終了を行いますが、この場合、CPUパイプラインを壊し、メモリーシーク問題を作成し、CPUが外部キャッシュからロードする必要があることを意味します。しかし、基本的には、正しいコードの抽象的な概念を実装するためだけに複雑なコードパスを作成するのではなく、ループまたは関数を終了するという目的を実行する必要があると思います。あなたが持っている唯一のツールがハンマーである場合、すべてが釘のように見えます。

0