web-dev-qa-db-ja.com

短絡評価、それは悪い習慣ですか?

私がしばらくの間知っていたが考慮されなかったことの1つは、ほとんどの言語で、演算子の順序に基づいてifステートメントで演算子を優先することができるということです。私はこれをnull参照の例外を防ぐ方法としてよく使用します。例:

if (smartphone != null && smartphone.GetSignal() > 50)
{
   // Do stuff
}

この場合、コードは最初にオブジェクトがnullではないことを確認し、次にこのオブジェクトが存在することを認識して使用します。最初のステートメントがfalseの場合、2番目のステートメントを評価しても意味がないため、null参照例外がスローされないことがわかっているため、この言語は賢いです。これは、andおよびor演算子と同じように機能します。

これは、インデックスが配列の境界内にあることを確認するなど、他の状況でも役立ちます。そのような手法は、Java、C#、C++、Python、Matlabなどのさまざまな言語で実行できます。

私の質問は:この種のコードは悪い習慣の表現ですか?この悪い習慣は隠れた技術的な問題から発生しますか(つまり、最終的にエラーになる可能性があります)、それとも読みやすさの問題につながりますか?他のプログラマーのために?紛らわしいですか?

89
innova

いいえ、これは悪い習慣ではありません。条件式の短絡に依存することは、広く受け入れられており、便利な手法です この動作を保証する言語 (現代の言語の大部分を含む)を使用している限り。

あなたのコード例は非常に明確であり、実際、それは多くの場合それを書くための最良の方法です。代わりの方法(ネストされたifステートメントなど)は、煩雑で複雑なため、理解が難しくなります。

いくつかの注意事項:

複雑さと読みやすさに関して常識を使用してください

以下はそれほど良い考えではありません:

if ((text_string != null && replace(text_string,"$")) || (text_string = get_new_string()) != null)

コードの行を削減する多くの「巧妙な」方法と同様に、この手法に過度に依存すると、理解しにくく、エラーが発生しやすいコードになる可能性があります。

エラーを処理する必要がある場合は、多くの場合より良い方法があります

スマートフォンが通常のプログラム状態である限り、あなたの例は問題ありません。ただし、これがエラーの場合は、よりスマートなエラー処理を組み込む必要があります。

同じ条件の繰り返しチェックを避ける

ニールが指摘するように、異なる条件文で同じ変数を繰り返しチェックすることは、設計上の欠陥の証拠である可能性が最も高いです。 smartphonenullの場合に実行してはならないコードに10個の異なるステートメントが散在している場合は、毎回変数をチェックするよりもこれを処理するより良い方法があるかどうかを検討してください。

これは、具体的には短絡の問題ではありません。それは反復的なコードのより一般的な問題です。ただし、例のステートメントのように多くの繰り返しステートメントを含むコードを表示することは非常に一般的であるため、言及する価値があります。

125
user82096

_&&_なしのCスタイル言語を使用していて、質問と同じコードを実行する必要があるとしましょう。

あなたのコードは次のようになります:

_if(smartphone != null)
{
  if(smartphone.GetSignal() > 50)
  {
    // Do stuff
  }
}
_

このパターンは非常によくなります。

ここで、仮想言語のバージョン2.0が_&&_を導入するとします。それがどれほどクールだったと思いますか!

_&&_は、上記の例で行っていることを行うための認識された慣用的な手段です。これを使用することは悪い習慣ではありません。実際、上記のような場合に使用しないことは悪い習慣です。このような言語の経験豊富なコーダーは、elseがどこにあるのか、または通常のファッション。

最初のステートメントがfalseの場合、2番目のステートメントを評価しても意味がないため、null参照例外がスローされないことがわかっているため、この言語は賢いです。

いいえ、あなたは賢いです。なぜなら、最初のステートメントがfalseの場合、2番目のステートメントを評価しても意味がないことを知っていたからです。言語は岩の箱のように馬鹿げており、あなたが言うようにそれを行いました。 (ジョン・マッカーシーはさらに賢く、短絡評価が言語にとって有用であることを理解していました)。

あなたが賢いことと言語が賢いことの違いは重要です。なぜなら、良い実践と悪い実践は、必要に応じて賢いということに帰着することが多いからです。

考慮してください:

_if(smartphone != null && smartphone.GetSignal() > ((range = (range != null ? range : GetRange()) != null && range.Valid ? range.MinSignal : 50)
_

これにより、rangeをチェックするようにコードが拡張されます。 rangeがnullの場合、GetRange()を呼び出して設定しようとしますが、失敗する可能性があるため、rangeはnullである可能性があります。この後、範囲がnullではなく、Validの場合、そのMinSignalプロパティが使用されます。それ以外の場合、デフォルトの_50_が使用されます。

これは_&&_にも依存しますが、1行に入れるのはおそらく賢すぎます(正しいことを100%確信しているわけではありません。その事実が私のポイントを示しているので、再確認するつもりはありません)。 。

ここで問題となるのは_&&_ではありませんが、1つの式に多くを入れるためにそれを使用できること(良いこと)は、理解しにくい式を不必要に(悪いこと)書く能力を高めます。

また:

_if(smartphone != null && (smartphone.GetSignal() == 2 || smartphone.GetSignal() == 5 || smartphone.GetSignal() == 8 || smartPhone.GetSignal() == 34))
{
  // do something
}
_

ここでは、_&&_の使用と特定の値のチェックを組み合わせています。これは電話信号の場合には現実的ではありませんが、他の場合には発生します。ここで、それは私が十分に利口ではない例です。以下を実行した場合:

_if(smartphone != null)
{
  switch (smartphone.GetSignal())
  {
    case 2: case 5: case 8: case 34:
      // Do something
      break;
  }
}
_

私は読みやすさとパフォーマンスの両方で得ているでしょう(GetSignal()への複数の呼び出しは最適化されていなかったでしょう)。

ここでも問題は、特定のハンマーを取り、他のすべてを釘として見るほど_&&_ではありません。それを使用しないことは、それを使用するよりも良いことをさせてくれました。

ベストプラクティスから離れる最後のケースは次のとおりです。

_if(a && b)
{
  //do something
}
_

に比べ:

_if(a & b)
{
  //do something
}
_

なぜ後者を支持するのかについての古典的な議論は、bがtrueであるかどうかにかかわらず、aの評価にいくつかの副作用があるということです。私はそれに同意しません。その副作用が非常に重要である場合は、別のコード行で発生させてください。

ただし、効率の点では、2つのうちどちらかが優れている可能性があります。最初のコードは明らかに少ないコードを実行し(1つのコードパスでbをまったく評価しない)、bの評価にかかる時間を節約できます。

ただし、最初のブランチにはもう1つブランチがあります。 _&&_のない架空のCスタイル言語で書き直すかどうかを検討してください。

_if(a)
{
  if(b)
  {
    // Do something
  }
}
_

その余分なifは、_&&_の使用では隠されていますが、まだ残っています。そのため、分岐予測が発生し、その結果、分岐の誤予測が発生する可能性があります。

そのため、if(a & b)コードの方が効率的な場合があります。

ここで私はif(a && b)がまだ最初のベストプラクティスアプローチであると言います。より一般的なのは、場合によっては実行可能な唯一の方法です(baはfalse)であり、多くの場合より高速です。ただし、場合によってはif(a & b)が最適化に役立つことがよくあります。

26
Jon Hanna

これをさらに進めることができます。一部の言語では、慣用的な方法で物事を行うことができます。条件付きステートメントの外でも短絡評価を使用でき、それら自体が条件付きステートメントの形式になります。例えば。 Perlでは、関数が失敗したときに偽の(そして成功したときに真の)何かを返すのは慣用的です。

open($handle, "<", "filename.txt") or die "Couldn't open file!";

慣用的です。 openは成功するとゼロ以外の値を返しますが(dieの後のor部分は決して発生しないため)、失敗すると未定義になり、その後dieを呼び出すと起こります(これはPerlが例外を発生させる方法です)。

それは、誰もがそれを行う言語では問題ありません。

10
RemcoGerlich

私は一般的に dan1111の答え に同意しますが、カバーしない特に重要なケースが1つあります。それは、smartphoneが同時に使用されるケースです。そのシナリオでは、この種のパターンは、見つけにくいバグのよく知られた原因です。

問題は、短絡評価がアトミックでないことです。スレッドはsmartphonenullであることを確認でき、別のスレッドが入ってそれを無効にできるため、すぐにGetSignal()を試行して爆破します。

しかし、もちろん、それは星が整列し、スレッドのタイミングがちょうど正しい場合にのみ行われます。

したがって、この種のコードは非常に一般的で便利ですが、この厄介なバグを正確に防止できるように、この警告について知っておくことが重要です。

6
Telastyn

最初に1つの条件をチェックし、最初の条件が成功した場合にのみ2番目の条件をチェックしたい状況はたくさんあります。純粋に効率のために(最初の条件が既に失敗した場合は2番目の条件をチェックする意味がないため)、そうでない場合はプログラムがクラッシュし(最初にNULLのチェック)、そうでない場合は結果がひどいことがあります。 (IF(ドキュメントを印刷したい)AND(ドキュメントの印刷に失敗しました))次にエラーメッセージを表示します-ドキュメントを印刷したくない場合は、ドキュメントを印刷したくない場合に印刷が失敗したかどうかを確認します最初の場所!

したがって、論理式の短絡評価を保証する必要が非常にありました。これはPascal(保証されていない短絡評価を行った)には存在せず、大きな問題でした。 AdaとCで初めて見ました。

これが「悪い習慣」であると本当に思う場合は、適度に複雑なコードを使用して、短絡評価なしで正常に機能するようにコードを書き直し、戻って、気に入った方法を教えてください。これは、呼吸すると二酸化炭素が発生すると言われ、呼吸が悪い習慣かどうかを尋ねるようなものです。それなしで数分間お試しください。

3
gnasher729

他の回答で言及されていない1つの考慮事項:これらのチェックがあると、 Null Object 設計パターンへの可能なリファクタリングのヒントになることがあります。例えば:

if (currentUser && currentUser.isAdministrator()) 
  doSomething();

次のように単純化できます:

if (currentUser.isAdministrator())
  doSomething ();

currentUserがデフォルトで一部の「匿名ユーザー」または「nullユーザー」に設定されている場合、ユーザーがログインしていない場合はフォールバック実装が使用されます。

常にコードのにおいではなく、考慮すべきこと。

2
Pete