web-dev-qa-db-ja.com

if(if else)elseを処理するエレガントな方法

これは些細なことですが、このようなコードを記述しなければならないたびに、繰り返しが気になりますが、どの解決策も悪くないかどうかはわかりません。

if(FileExists(file))
{
    contents = OpenFile(file); // <-- prevents inclusion in if
    if(SomeTest(contents))
    {
        DoSomething(contents);
    }
    else
    {
        DefaultAction();
    }
}
else
{
    DefaultAction();
}
  • この種のロジックに名前はありますか?
  • 私はちょっとOCDすぎますか?

好奇心のために、私は悪意のあるコードの提案を受け入れます...

165
Benjol

それを抽出して関数(メソッド)を分離し、returnステートメントを使用します。

_if(FileExists(file))
{
    contents = OpenFile(file); // <-- prevents inclusion in if
    if(SomeTest(contents))
    {
        DoSomething(contents);
        return;
    }
}

DefaultAction();
_

または、おそらくより良い、取得するコンテンツとその処理を分離します。

_contents_t get_contents(name_t file)
{
    if(!FileExists(file))
        return null;

    contents = OpenFile(file);
    if(!SomeTest(contents)) // like IsContentsValid
        return null;

    return contents;
}

...

contents = get_contents(file)
contents ? DoSomething(contents) : DefaultAction();
_

更新:

なぜ例外ではなく、OpenFileがIO例外をスローしないのか:
これは、ファイルIOに関する質問ではなく、本当に一般的な質問だと思います。 FileExistsOpenFileなどの名前は混乱を招く可能性がありますが、FooBarなどに置き換えると、DefaultActionDoSomethingと同じ頻度で呼び出される可能性があるため、例外ではない場合もあります。 PéterTörökがこれについて書きました 彼の答えの終わりに

2番目のバリアントに三項条件演算子がある理由:
[C++]タグがある場合、条件部分にifを宣言してcontentsステートメントを記述しました。

_if(contents_t contents = get_contents(file))
    DoSomething(contents);
else
    DefaultAction();
_

ただし、他の(Cのような)言語の場合、if(contents) ...; else ...;は、3項条件演算子を使用した式ステートメントとまったく同じですが、長くなります。コードの主要部分は_get_contents_関数であったため、短いバージョンを使用しました(そしてcontentsタイプも省略しました)。とにかく、それはこの質問を超えています。

97
Abyx

使用しているプログラミング言語が(0)短絡バイナリ比較(つまり、SomeTestが呼び出されない場合、FileExistsがfalseを返す場合)と(1)代入は値( OpenFilecontentsに割り当てられ、その値が引数としてSomeTest)に渡されます。次のようなものを使用できますが、単一の=は意図的なものです。

if( FileExists(file) && SomeTest(contents = OpenFile(file)) )
{
    DoSomething(contents);
}
else
{
    DefaultAction();
}

Ifがどの程度複雑であるかに応じて、フラグ変数(この場合、エラーDefaultActionを処理するコードで成功/失敗条件のテストを分離する)を使用する方が良い場合があります。

56
frozenkoi

DefaultActionの呼び出しの繰り返しよりも真剣にスタイル自体が発生します。これは、コードが非直交で記述されているためです(直交して記述する理由については この答え を参照してください)。

ファイルがネットワークディスクに保存されている場合、ファイルを開かないという新しい要件が導入されたときに、非直交コードが悪い理由を示すために、元の例を検討してください。それでは、コードを次のように更新します。

if(FileExists(file))
{
    if(! OnNetworkDisk(file))
    {
        contents = OpenFile(file); // <-- prevents inclusion in if
        if(SomeTest(contents))
        {
            DoSomething(contents);
        }
        else
        {
            DefaultAction();
        }
    }
    else
    {
        DefaultAction();
    }
}
else
{
    DefaultAction();
}

ただし、2Gbを超える大きなファイルも開かないようにするという要件もあります。さて、もう一度更新します:

if(FileExists(file))
{
    if(LessThan2Gb(file))
    {
        if(! OnNetworkDisk(file))
        {
            contents = OpenFile(file); // <-- prevents inclusion in if
            if(SomeTest(contents))
            {
                DoSomething(contents);
            }
            else
            {
                DefaultAction();
            }
        }
        else
        {
            DefaultAction();
        }
    else
    {
        DefaultAction();
    }
}
else
{
    DefaultAction();
}

このようなコードスタイルは、メンテナンスの大きな負担になることは明らかです。

適切に直角に記述されたここでの回答には Abyxの2番目の例Jan Hudecの回答 があるので、ここでは繰り返さないで、2つの要件を追加することを指摘します。答えはちょうどだろう

if(! LessThan2Gb(file))
    return null;

if(OnNetworkDisk(file))
    return null;

(またはgoto notexists; の代わりに return null;)、追加された行以外のコードには影響しません。例えば。直交。

テストする場合、一般的なルールは 通常のケースではなく、例外をテストする にする必要があります。

26
hlovdal

明らかに:

Whatever(Arguments)
{
    if(!FileExists(file))
        goto notexists;
    contents = OpenFile(file); // <-- prevents inclusion in if
    if(!SomeTest(contents))
        goto notexists;
    DoSomething(contents);
    return;
notexists:
    DefaultAction();
}

あなたは邪悪な解決策に対してさえもオープンであると言ったので、邪悪なgotoカウントを使用していませんか?

実際、状況に応じて、このソリューションは、アクションを2回実行することや、余分な変数を追加することよりも害が少ない可能性があります。私はそれを関数にラップしました。これは長い関数の真ん中では絶対に大丈夫ではないからです(特に真ん中に戻るため)。でも、長い機能よりもOKじゃない、期間。

例外があると、特に条件が満たされない場合にOpenFileとDoSomethingが単に例外をスローできるため、明示的にチェックする必要がないため、例外が読みやすくなります。一方、C++では、JavaおよびC#で例外をスローするのは遅い操作なので、パフォーマンスの点からはgotoの方が適しています。


「悪」に関する注記: C++ FAQ 6.15 は「悪」を次のように定義します。

これはそのようなmostの時間を避けるべきものであることを意味します、しかしあなたが避けるべきではない何かすべて時間。たとえば、これらの「邪悪な」ものは、​​「邪悪な選択肢の中で最も邪悪なもの」ではない場合はいつでも使用することになります。

これは、このコンテキストのgotoにも当てはまります。ほとんどの場合、構造化フロー制御コンストラクトの方が優れていますが、条件での割り当て、3レベル以上の深さでのネスト、コードの重複、または長い条件など、独自の悪が蓄積しすぎる状況に陥った場合、gotoは、単に害が少なくなる場合があります。

25
Jan Hudec

1つの可能性:

_boolean handled = false;

if(FileExists(file))
{
    contents = OpenFile(file); // <-- prevents inclusion in if
    if(SomeTest(contents))
    {
        DoSomething(contents);
        handled = true;
    }
}
if (!handled)
{
    DefaultAction();
}
_

もちろん、これにより、コードは別の方法で少し複雑になります。したがって、それは主にスタイルの質問です。

別のアプローチは例外を使用することです、例えば:

_try
{
    contents = OpenFile(file); // throws IO exception if file not found
    DoSomething(contents); // calls SomeTest() and throws exception on failure
}
catch(Exception e)
{
    DefaultAction();
    // and the exception should be at least logged...
}
_

これは単純に見えますが、以下の場合にのみ適用されます

  • どのような例外が予想されるかを正確に把握しており、DefaultAction()がそれぞれに当てはまる
  • ファイル処理が成功することを期待しており、ファイルが見つからないか、失敗したSomeTest()は明らかにエラー状態であるため、例外をスローするのが適切です。
12
Péter Török
function FileContentsExists(file) {
    return FileExists(file) ? OpenFile(file) : null;
}

...

contents = FileContentExists(file);
if(contents && SomeTest(contents))
{
    DoSomething(contents);
}
else
{
    DefaultAction();
}
12
herby

関数は1つのことを行う必要があります。彼らはそれをうまくやるべきです。彼らはそれだけをすべきです。
クリーンコードでのRobert Martin

一部の人々はそのアプローチが少し極端であると感じますが、それも非常にきれいです。 Pythonで説明してみましょう:

_def processFile(self):
    if self.fileMeetsTest():
        self.doSomething()
    else:
        self.defaultAction()

def fileMeetsTest(self):
    return os.path.exists(self.path) and self.contentsTest()

def contentsTest(self):
    with open(self.path) as file:
        line = file.readline()
        return self.firstLineTest(line)
_

関数は1つのことを行うべきだと彼が言ったとき、彼は1つのことを意味しますprocessFile()は、テストの結果に基づいてアクションを選択します。それだけです。 fileMeetsTest()は、テストのすべての条件を組み合わせたものです。 contentsTest()は、最初の行をfirstLineTest()に転送します。それだけです。

多くの関数のように見えますが、実質的には英語のように読めます。

ファイルを処理するには、テストを満たしているかどうかを確認します。ある場合は、何かを行います。それ以外の場合は、デフォルトのアクションを実行します。ファイルは、存在する場合はテストに適合し、コンテンツテストに合格します。内容をテストするには、ファイルを開いて最初の行をテストします。最初の行のテスト...

確かにこれは少し言い回しですが、メンテナが詳細を気にしない場合は、processFile()の4行のコードのあとで読み取りを停止でき、高度な知識を持っていることに注意してください関数の機能の。

11
Karl Bielefeldt

これは抽象化のより高いレベルにあります:

if (WeCanDoSomething(file))
{
   DoSomething(contents);
}
else
{
   DefaultAction();
} 

そして、これは詳細を埋めます。

boolean WeCanDoSomething(file)
{
    if FileExists(file)
    {
        contents = OpenFile(file);
        return (SomeTest(contents));
    }
    else
    {
        return FALSE;
    }
}
11
Jeanne Pindar

これが何と呼ばれるかについては、コードがより多くの要件を処理するように成長するにつれて、矢じりアンチパターンに簡単に発展する可能性があります(提供される回答で示されているように) https://softwareengineering.stackexchange.com/a/122625/33922 )そして、矢印に似たネストされた条件ステートメントを持つコードの巨大なセクションを持つという罠に陥ります。

次のようなリンクを参照してください。

http://codinghorror.com/blog/2006/01/flattening-arrow-code.html

http://lostechies.com/chrismissal/2009/05/27/anti-patterns-and-worst-practices-the-arrowhead-anti-pattern/

Googleで見つかるこのパターンやその他のアンチパターンについては、他にもたくさんあります。

これに関してジェフが彼のブログで提供しているいくつかの素晴らしいヒントがあります。

1)条件をガード句に置き換えます。

2)条件付きブロックを個別の関数に分解します。

3)ネガティブチェックをポジティブチェックに変換する

4)常に便宜的に関数からできるだけ早く戻ります。

早期返品に関するSteve McConnellsの提案に関するJeffのブログのコメントもいくつか参照してください。

「読みやすさを向上させるときにreturnを使用する:特定のルーチンでは、答えがわかったらすぐにそれを呼び出し元のルーチンに戻したい場合があります。ルーチンが定義されていて、一度クリーンアップする必要がない場合エラーを検出し、すぐに戻らないということは、より多くのコードを書く必要があることを意味します。」

...

「各ルーチンのリターンの数を最小限に抑える:ルーチンの下部を読むと、それがどこかで戻った可能性に気づいていないと、ルーチンを理解するのが難しくなります。そのため、リターンは慎重に使用してください。読みやすさ。」

15年ほど前に教わったことから、私は常に関数ごとの1エントリ/出口理論にサブスクライブしました。これはコードを非常に読みやすくするだけだと思います。

6
Mr Moose

これは、DRY、no-gotoおよびno-multiple-returnsルールに準拠し、私の意見ではスケーラブルで読み取り可能です。

success = FileExists(file);
if (success)
{
    contents = OpenFile(file);
    success = SomeTest(contents);
}
if (success)
{
    DoSomething(contents);
}
else
{
    DefaultAction();
}
6
mouviciel

この特定のケースでは、答えは簡単です...

FileExistsOpenFileの間には競合状態があります。ファイルが削除されるとどうなりますか?

この特定のケースに対処する唯一の健全な方法は、FileExistsをスキップすることです。

contents = OpenFile(file);
if (!contents) // open failed
    DefaultAction();
else (SomeTest(contents))
    DoSomething(contents);

これはこの問題をきちんと解決しますandはコードをよりきれいにします。

一般的に:問題を再考し、問題を完全に回避する別のソリューションを考案してください。

3
Martin Wickman

それを別のメソッドに抽出してから:

if(!FileExists(file))
{
    DefaultAction();
    return;
}

contents = OpenFile(file);
if(!SomeTest(contents))
{
    DefaultAction();
    return;
}

DoSomething(contents);

これにより、

if(!FileExists(file))
{
    DefaultAction();
    return Result.FileNotFound;
}

contents = OpenFile(file);
if(!SomeTest(contents))
{
    DefaultAction();
    return Result.TestFailed;
}

DoSomething(contents);
return Result.Success;            

次に、DefaultAction呼び出しを削除し、呼び出し元に対してDefaultActionを実行したままにすることができます。

Result OurMethod(file)
{
    if(!FileExists(file))
    {
        return Result.FileNotFound;
    }

    contents = OpenFile(file);
    if(!SomeTest(contents))
    {
        return Result.TestFailed;
    }

    DoSomething(contents);
    return Result.Success;            
}

void Caller()
{
    // something, something...

    var result = OurMethod(file);
    // if (result == Result.FileNotFound || result == Result.TestFailed), or just
    if (result != Result.Success)        
    {
        DefaultAction();
    }
}

Jeanne Pindarのアプローチ も好きです。

3
Konrad Morawski

Elseの数が多すぎないようにしたい場合のもう1つの可能性は、elseの使用を完全に削除して、余​​分なreturnステートメントをスローすることです。 Elseは、2つ以上のアクションの可能性があるかどうかを判断するために、より複雑なロジックが必要でない限り、余計なものです。

したがって、あなたの例は次のようになるでしょう:

void DoABunchOfStuff()
{
    if(FileExists(file))
    {
        DoSomethingWithFileContent(file);
        return;
    }

    DefaultAction();
}

void DoSomethingWithFileContent(file)
{        
    var contents = GetFileContents(file)

    if(SomeTest(contents))
    {
        DoSomething(contents);
        return;
    }

    DefaultAction();
}

AReturnType GetFileContents(file)
{
    return OpenFile(file);
}

個人的にはelse句を使用してもかまいません。ロジックがどのように機能するかが明示されているため、コードの可読性が向上します。ただし、一部のコード美化ツールは、ネストロジックを阻止するために、単一のifステートメントに簡略化することを好みます。

2
S.Robins

通常、サンプルコードに示されているケースは、単一のifステートメントに減らすことができます。多くのシステムでは、ファイルがまだ存在しない場合、file-open関数は無効な値を返します。これがデフォルトの動作になる場合があります。それ以外の場合は、引数を介して指定する必要があります。これは、FileExistsテストを削除できることを意味します。これは、存在テストとファイルオープンの間のファイル削除に起因する競合状態にも役立ちます。

file = OpenFile(path);
if(isValidFileHandle(file) && SomeTest(file)) {
    DoSomething(file);
} else {
    DefaultAction();
}

複数の連鎖不可能なテストの問題を完全に回避するため、これは抽象化レベルの混合の問題に直接対処しませんが、ファイル存在テストを廃止することは、抽象化レベルの分離と互換性がありません。無効なファイルハンドルは「false」と同等であり、ファイルハンドルがスコープ外になると閉じると仮定します。

OpenFileIfSomething(path:String) : FileHandle {
    file = OpenFile(path);
    if (file && SomeTest(file)) {
        return file;
    }
    return null;
}

...

if ((file = OpenFileIfSomething(path))) {
    DoSomething(file);
} else {
    DefaultAction();
}
2
outis

私はfrozenkoiに同意していますが、とにかくC#の場合、TryParseメソッドの構文に従うと役立つと思いました。

if(FileExists(file) && TryOpenFile(file, out contents))
    DoSomething(contents);
else
    DefaultAction();
bool TryOpenFile(object file, out object contents)
{
    try{
        contents = OpenFile(file);
    }
    catch{
        //something bad happened, computer probably exploded
        return false;
    }
    return true;
}
2
Biff MaGriff

明らかな何が悪いのか

if(!FileExists(file)) {
    DefaultAction();
    return;
}
contents = OpenFile(file);
if(!SomeTest(contents))
{
    DefaultAction();
    return;
}        
DoSomething(contents);

それは私にはかなり標準的なようです?多くのささいなことが発生しなければならないそのような大きな手順では、いずれかの失敗は後者を防ぐでしょう。それがオプションである場合、例外はそれを少しすっきりさせます。

1
Steve Bennett

もちろん、このようなシナリオでしか実行できませんが、次の方法があります。

interface File<T> {
    function isOK():Bool;
    function getData():T;
}

var appleFile:File<Apple> = appleStorage.get(fileURI);
if (appleFile.isOK())
    eat(file.getData());
else
    cry();

追加のフィルターが必要な場合があります。次にこれを行います:

var appleFile = appleStorage.get(fileURI, isEdible);
//isEdible is of type Apple->Bool and will be used internally to answer to the isOK call
if (appleFile.isOK())
    eat(file.getData());
else
    cry();

これも意味があるかもしれませんが:

function eat(Apple:Apple) {
     if (isEdible(Apple)) 
         digest(Apple);
     else
         die();
}
var appleFile = appleStorage.get(fileURI);
if (appleFile.isOK())
    eat(appleFile.getData());
else
    cry();

どちらがベストか?それは、あなたが直面している現実の問題に依存します。
しかし、注意しなければならないのは、構成と多態性で多くのことができるということです。

1
back2dos

単一の関数でやりすぎているため、コードは醜いです。ファイルを処理するか、デフォルトのアクションを実行する必要があるため、まず次のように言います。

_if (!ProcessFile(file)) { 
  DefaultAction(); 
}
_

PerlとRubyプログラマーが書くprocessFile(file) || defaultAction()

次に、ProcessFileを記述します。

_if (FileExists(file)) { 
  contents = OpenFile(file);
  if (SomeTest(contents)) {
    processContents(contents);
    return true;
  }
}
return false;
_
1
kevin cline

明らかに、最もエレガントで簡潔なソリューションは、プリプロセッサマクロを使用することです。

#define DOUBLE_ELSE(CODE) else { CODE } } else { CODE }

これにより、次のような美しいコードを記述できます。

if(FileExists(file))
{
    contents = OpenFile(file);
    if(SomeTest(contents))
    {
        DoSomething(contents);
    }
    DOUBLE_ELSE(DefaultAction();)

この手法を頻繁に使用する場合、自動フォーマットに依存するのは難しい場合があり、一部のIDEは、誤って誤った形式であると誤って想定していることについて少し怒鳴るかもしれません。そしてことわざにあるように、すべてはトレードオフですが、繰り返しコードの悪を避けるために支払うことは悪い代償ではないと思います。

0
Peter Olson

ネストされたIFを減らすには:

1 /早期復帰。

2 /複合式(短絡対応)

だから、あなたの例は次のようにリファクタリングされるかもしれません:

if( FileExists(file) && SomeTest(contents = OpenFile(file)) )
{
    DoSomething(contents);
    return;
}
DefaultAction();
0
DQ_vn

これは古い質問だと思いますが、言及されていないパターンに気づきました。主に、後で呼び出すメソッドを決定するために変数を設定します(if ... else ...の外)。

これは、コードを扱いやすくするための別の見方と同じです。また、呼び出される別のメソッドを追加したり、特定の状況で呼び出す必要がある適切なメソッドを変更したりする場合にも対応できます。

メソッドのすべての記述を置き換える必要がなく(いくつかのシナリオが欠落している可能性があります)、それらはすべてif ... else ...ブロックの最後にリストされ、読み取りと変更が簡単です。たとえば、複数のメソッドが呼び出される可能性がある場合にこれを使用する傾向がありますが、ネストされたif ... else ...内で、複数の一致でメソッドが呼び出されることがあります。

状態を定義する変数を設定すると、多くの深くネストされたオプションがあり、何かが実行される(または実行されない)ときに状態を更新できます。

これは、「DoSomething」が発生したかどうかを確認する質問で尋ねた例のように使用できます。発生していない場合は、デフォルトのアクションを実行します。または、呼び出すメソッドごとに状態を設定し、該当する場合に設定してから、if ... else ...の外で該当するメソッドを呼び出すこともできます。

ネストされたif ... else ...ステートメントの最後で、状態をチェックし、それに応じて動作します。これは、適用する必要のあるすべての場所ではなく、メソッドの単一の言及のみが必要であることを意味します。

bool ActionDone = false;

if (Method_1(object_A)) // Test 1
{
    result_A = Method_2(object_A); // Result 1

    if (Method_3(result_A)) // Test 2
    {
        Method_4(result_A); // Action 1
        ActionDone = true;
    }
}

if (!ActionDone)
{
    Method_5(); // Default Action
}
0
Steve Padmore

私も使用する「return」の例をたくさん見ましたが、新しい関数の作成を避け、代わりにループを使用したい場合があります。

while (1) {
    if (FileExists(file)) {
        contents = OpenFile(file);
        if (SomeTest(contents)) {
           DoSomething(contents);
           break;
        } 
    }
    DefaultAction();
    break;
}

行数を減らしたい場合、または無限ループが嫌いな場合は、ループの種類を "do ... while(0)"に変更して、最後の "ブレーク"を回避できます。

0
XzKto

このソリューションはどうですか:

content = NULL; //I presume OpenFile returns a pointer 
if(FileExists(file))
    contents = OpenFile(file);
if(content != NULL && SomeTest(contents))
    DoSomething(contents);
else
    DefaultAction();

私はOpenFileがポインターを返すと仮定しましたが、これは戻り値ではないいくつかのデフォルト値(エラーコードなど)を指定することにより、戻り値の型でも機能します。

もちろん、私はNULLポインタのSomeTestメソッドを介していくつかの可能なアクションを期待していません(しかし、あなたは決して知りません)ので、これは、SomeTest(contents)呼び出しのNULLポインタの追加のチェックと見なすこともできます。

0
chedi