web-dev-qa-db-ja.com

「if-elseを避ける」アドバイスの明確化

クリーンなコードの専門家は、判読不能なコードを作成しているため、if/elseを使用しないことをお勧めします。彼らはむしろIFを使用し、メソッドの最後まで実際に必要なく待たないことを提案しています。

さて、このif/elseのアドバイスは私を混乱させます。私はif/elseまったく(!)を使用してはならない、またはif/elseのネストを避けるべきだと言っていますか?

また、それらがif/elseの入れ子を参照している場合、1つの入れ子でさえ実行すべきではないか、それを最大2つの入れ子に制限する必要がありますか(推奨)。

私が単一の入れ子と言うとき、私はこれを意味します:

    if (...) {

        if (...) {

        } else {

        }

    } else {

    }

[〜#〜]編集[〜#〜]

また、Resharperなどのツールは、if/elseステートメントの再フォーマットを提案します。それらは通常、ifの独立したステートメント、さらには3値式に合わせて調整されます。

39
deviDave

このアドバイスは、循環的複雑度または条件付き複雑度チェック このwikiページ と呼ばれるソフトウェアメトリックから来たと思います=

定義:

ソースコードのセクションの循環的複雑度は、ソースコードを通る線形独立パスの数のカウントです。たとえば、ソースコードにIFステートメントやFORループなどの決定ポイントが含まれていない場合、コードを通るパスは1つだけなので、複雑度は1になります。コードに単一の条件を含む単一のIFステートメントがある場合、コードには2つのパスがあり、1つはIFステートメントがTRUEと評価され、もう1つはIFステートメントがFALSEと評価されます。

そう

If/elseをまったく使用しない(!)か、またはif/elseのネストを回避するべきだと彼らは言っていますか?

いいえ、if/elseをまったく避けるべきではありません。ただし、コードのCyclomaticの複雑さに注意する必要があります。コードが複雑になるほど、欠陥が増える傾向があります。 this によると、正しい制限は11になります。 Code Completeと呼ばれるツールは、そのようにスコアを分類します Ref。

  • 0-5-ルーチンはおそらく問題ありません
  • 6-10-ルーチンを簡略化する方法について考え始める
  • 10 +-ルーチンの一部を2番目のルーチンに分割し、最初のルーチンから呼び出します

循環的複雑度を「基本的に」計算する方法:

例から here :次の疑似コードを考えます:

_if (A=354) {

    if (B>C) {/*decision 1*/ A=B} else {/*decision 2*/ A=C}

   }/*decision 3 , in case A is NOT equal 354*/ 
print A
_

このメトリックはプログラム/コードの決定の数に関係していることに注意してください。上のコードから、3つの決定(Aの3つの値)があると言えます。

正式に計算してみましょう。次のコードの制御フローを検討してください。 enter image description here

次に、複雑度Mは次のように定義されます* Ref

_M = E − N + 2P
_

どこ

_E = the number of edges of the graph
N = the number of nodes of the graph
P = the number of connected components (exit nodes).
_

E(flow lines) = 8N (nodes)= 7P (exit nodes) =1を適用して、次にM = 8-7 + (2*1) = 3

*別の定義も記載されていますが、近いです:_M = E − N + P_理論については 参照 を確認してください。

静的コード分析ツール の多くは、コードの循環的複雑度を計算できることに注意してください。

43
Abdurahman

IMO、以前の回答はエッジのケースととにかく書き換えられるコードに焦点を当てています。 if/elseifまたは別の言語構成要素で置き換えられるはずのほとんどすべてのケースを以下に示します。

注:私は回答コミュニティwikiを作成します。それを自由に変更して、if/elseが通常不良コードにつながるケースの他の例を追加してください。

初心者、 警告されますif/elseは、注意深く使用しないと、読み取り不可能なコードを作成する可能性があります。次の例は、if/elseが簡単に誤用される最も一般的なケースを示しています。

1.ガード条項

私はこのようなものを見た回数を数えるのを止めました:

void Demo(...)
{
    if (ok_to_continue)
    {
        ... // 20 lines here.
    }
    else
    {
        throw new SomeException();
    }
}

余分なインデントは必要ありません。同じコードを次のように書くこともできます:

void Demo(...)
{
    if (!ok_to_continue)
    {
        throw new SomeException();
    }

    ... // No indentation for the next 20 lines.
}

ルール:elseチェックを反転してエラーを処理し、すぐに終了することでifステートメントを削除できる場合(それ以外の場合は続行)、次にそうする

2.コードの複製

これもかなり頻繁です。

if (!this.useAlternatePanel)
{
    currentInput = this.defaultTextBox.Text;
    bool missingInput = currentInput.Length == 0;
    this.defaultProcessButton.Enabled = !missingInput;
    this.defaultMissingInputHelpLabel.Visible = missingInput;
}
else
{
    currentInput = this.alternateTextBox.Text;
    bool missingInput = currentInput.Length == 0;
    this.alternateProcessButton.Enabled = !missingInput;
    this.alternateMissingInputHelpLabel.Visible = missingInput;
}

これは初心者のコードの優れた例です。初心者は、そのようなコードをリファクタリングする方法を常に見るとは限らないためです。方法の1つは、コントロールのマップを作成することです。

var defaultControls = new CountriesProcessingControls(
    this.defaultTextBox,
    this.defaultProcessButton,
    this.defaultMissingInputHelpLabel);

var alternateControls = new CountriesProcessingControls(
    this.alternateTextBox,
    this.alternateProcessButton,
    this.alternateMissingInputHelpLabel);

void RefreshCountriesControls(CountriesProcessingControls controls)
{
    currentInput = controls.textBox.Text;
    bool missingInput = currentInput.Length == 0;
    controls.processButton.Enabled = !missingInput;
    controls.missingInputHelpLabel.Visible = missingInput;
}

RefreshCountriesControls(this.useAlternatePanel ? alternateControls : defaultControls);

ルール:elseのブロックがifのブロックとほぼ同じである場合、それは間違っており、コードが重複しています。

3. 2つのことを実行する

反対も一般的です:

if (...)
{
    foreach (var dateInput = this.selectedDates)
    {
        var isConvertible = convertToDate(dateInput);
        if (!isConvertible)
        {
            this.invalidDates.add(dateInput);
        }
    }
}
else
{
    var languageService = this.initializeLanguageService();
    this.Translated = languageService.translate(this.LastInput);
    if (this.Translated != null)
    {
        this.NotificationScheduler.append(LAST_INPUT_TRANSLATED);
    }
}

if内のブロックは、else内のブロックとは何の関係もありません。同じ方法で2つのブロックを組み合わせると、ロジックが理解しにくくなり、エラーが発生しやすくなります。メソッドの名前を見つけ、適切にコメントすることも非常に困難です。

ルール:if/elseブロックで、共通点のないブロックを組み合わせることは避けてください。

4.オブジェクト指向プログラミング

この種のコーディングは、初心者の間でも頻繁に行われます。

// `animal` is either a dog or a cat.
if (animal.IsDog)
{
    animal.EatPedigree();
    animal.Bark();
}
else // The animal is a cat.
{
    animal.EatWhiskas();
    animal.Meow();
}

オブジェクト指向プログラミングでは、このコードは存在すべきではありません。代わりに次のように書く必要があります:

IAnimal animal = ...;
animal.Eat();
animal.MakeSound();

各クラス(Cat : IAnimalおよびDog : IAnimal)には、これらのメソッドの独自の実装があることを前提としています。

ルール:オブジェクト指向プログラミングを行う場合、関数(メソッド)を一般化し、継承とポリモーフィズムを使用します。

5.値を割り当てる

私はこれを初心者が書いたコードベースのどこにでも見ます:

int price;
if (this.isDiscount)
{
    price = -discount;
}
else
{
    price = unitCost * quantity;
}

これは人間向けに書かれていないので、ひどいコードです。

  • それは誤解を招くものです。ここでの主な操作は割引モードと非割引モードのどちらかを選択することであると思わせますが、実際の主な操作はpriceの割り当てです。

  • エラーが発生しやすいです。後で、1つの条件が変更された場合はどうなりますか? 2つの割り当てのうちの1つが削除された場合はどうなりますか?

  • さらに悪いコードを招きます。 int priceの代わりにint price = 0;を使用した、そのような多くの部分を見てきました。いずれかの割り当てが欠落している場合にコンパイラの警告を取り除くためです。警告を解決するのではなく非表示にすることは、実行できる最悪のことの1つです。

  • 小さなコードの重複があります。つまり、最初に入力する文字が増え、後で行う変更が増えます。

  • 無駄に長いです。

同じことは次のように書くことができます:

int price = this.isDiscount ? -discount : unitCost * quantity;

ルール:単純な値の割り当てには、if/elseブロックの代わりに三項演算子を使用します。

63

私はアブドゥルマンが彼の答えで言ったことを本当に気に入っています。

「if/elseの回避」ステートメントを確認するもう1つの方法は、決定の観点から考えることです。

目的は、コードに従うときの混乱を避けることです。ネストされた複数のif/elseステートメントがある場合、関数の主な目的を理解するのが難しくなります。コードを書くプログラマーはロジックnowを理解します。しかし、同じプログラマが1年か2年後にそのコードを再訪すると、その理解は一般的に忘れられます。コードを見ている新しいプログラマーは、何が起こっているのかを識別できなければなりません。したがって、目的は、複雑さを単純で理解しやすい機能に分解することです。目的は、決定を回避することではなく、理解しやすいロジックのフローを確立することです。

8
Paul Grimes

コードブロックのネスト(別名 "arrowhead"アンチパターン)は、右にシフトし続けるか、画面上の「実際の」コードの行の使用可能な幅を減らすか、インデントなどに違反する必要があるため、コードの可読性を低下させます。通常の場合、コードを読みやすくする書式ルール。

適例:

if (X) {
    if (Y) {
       DoXandY();
    } else {
       DoXOnly();
    }
} else {
   DoDefault();
}

これは簡単な例であり、このタイプのネストのいくつかのレベルでは問題はありません提供この構造のLOCの合計は画面の長さのみであり、関連するフォーマットは左側に余計な余白を追加しないでください(タブスペースが8スペース以上の可能性がある他のエディターでコードを表示する場合は特に、タブスペースの代わりに3または4のスペースを使用することを検討してください)。それを超えると、他のものがネストされている初期条件を忘れるか、簡単に確認できない場合があります。これにより、特にXとYが独立している構造がある場合、コードベースで「失われる」可能性があります。

if (X) {
    if (Y) {
       DoXandY();
    } else {
       DoXOnly();
    }
} else {
   if (Y) {
       DoYOnly();
    } else {
       DoDefault();
    }
}

OPの特定の質問に対するこの代替ユニバースでは、2つの「if(Y)」条件チェックがあり、実際のコードでネストされた親条件を確認できなかった場合は、コード内の実際の場所とは異なる場所にあるため、バグが発生します。

ネストを減らす1つの方法は、外部条件を内部条件に分散させることです。最初のスニペット(「DoYOnly()」なし)に戻り、次のことを考慮してください。

if (X && Y) {
    DoXandY();
} else if (X) {
    DoXOnly();
} else {
    DoDefault();
}

見て、入れ子なし! Xの追加評価を犠牲にして(Xを評価するのが計算的に複雑な場合は、事前に実行してブール変数に格納できます)、構造を単一レベルのif/else決定木にし、いつ各ブランチを編集すると、そのブランチに入るにはtrueでなければならない条件がわかります。 「else if」に到達するには、何がされなかった trueであるかを覚えておく必要がありますが、すべてが同じレベルであるため、意思決定ツリーの前のブランチを識別しやすくなります。または、すべてのステップで条件のより完全なバージョンをテストすることにより、elseキーワードをすべて削除できます。

if (X && Y) {
    DoXandY();
} 
if (X && !Y) {
    DoXOnly();
} 
if(!X) {
    DoDefault();
}

これで、実行する各ステートメントに対して何が真でなければならないかが正確にわかりました。ただし、これは他の読みやすさに関する一連の問題を突きつけ始めています。これらのすべてのステートメントが相互に排他的であることがわかりにくくなります(これは、「else」ブロックが行うことの一部です)。

ネストを含める他の方法には、1つ以上の条件を逆にすることが含まれます。関数のコードのほとんどまたはすべてが条件をテストするifステートメント内にある場合は、条件がnot trueであることをテストし、何でも実行することで、通常はそのレベルのネストを節約できます。 '関数から戻る前に、元のifステートメントを超えてelseブロックで実行します。例えば:

if(!X) {
    DoDefault();
    return;
}

if(Y) {
    DoXAndY();
} else {
    DoXOnly();
}

これは上記の両方のステートメントと同じように動作しますが、各条件を1回だけテストし、ネストされたifはありません。このパターンは「ガード句」と呼ばれることもあります。 Xがfalseの場合、最初のifステートメントの下のコードは実行されないので、ifステートメントは残りのコードを「ガード」していると言えます(そのため、残りのコードでXがtrueであることをテストする必要はありません。その行に到達すると、Xはtrueになります。そうでなければ、関数を早期に終了していたからです)。欠点は、これが大きな関数の小さな部分であるコードではうまく機能しないことです。上記のスニペット全体の後に「DoMoreWork()」関数があった場合、それをガード句コードと残りの条件ステートメントの下の両方に含める必要があります。または、「else if」パターンを使用して、guard句を残りのコードと統合できます。

if(!X) {
    DoDefault();        
} else if(Y) {
    DoXAndY();
} else {
    DoXOnly();
}

DoMoreWork();

この例では、Xがfalseの場合でも、DoMoreWork()が常に実行されます。 returnステートメントを使用して前のスニペットの下に同じ呼び出しを追加すると、Xがtrueの場合にのみDoMoreWorkが呼び出されますが、Yがtrueかfalseかは関係ありません。

7
KeithS

他の回答に加えて、アドバイスは_if/else_を_if/else if_で適切に置き換える必要があることを意味する場合もあります。後者は、より有益で読みやすくなります(もちろん、if (x > 0) else if (x <= 0)のような些細なケースは除きます)。

_if/else if_には、単に読みやすくすることよりも1つ多くの利点があります(ただし、これもかなりの利点です)。これは、条件に応じて3つ以上の分岐がある場合にプログラマーが間違いを回避するのに役立ちます。そのようなスタイルはmissedのケースをより明白にします。

1
superM

ifではなくelseステートメントのみを使用するようにアドバイスされている理由は、if this then for everything else do thisの従来のバグを回避するためです。 elseの範囲を予測することは困難です。

例えば;自動運転車のコントロールをプログラミングしているとしましょう。

if (light is red) then {
    stop car
} else {
    drive forward
}

論理は健全ですが。車が黄色の信号に到達すると、交差点を走行し続けます。減速して停止したはずの事故を起こしそう。

if/elseがなければ、ロジックは間違いを起こしにくくなります。

if(light is red) then {
  stop car
}
if(light is yellow) then {
  slow down
}
if(light is green) then {
  drive forward
}

私のコードは最高の自動運転車にはならないかもしれませんが。あいまいな条件と明示的な条件の違いを示すだけです。これも作者が言及しているようです。ロジックのショートカットにelseを使用しないでください。

1
Reactgular