例外を通常の制御フローとして使用しないのはなぜですか?


192

私がGoogledを使用した可能性があるすべての標準的な回答を回避するために、私はあなたが自由に攻撃できる例を提供します。

C#やJava(とあまりにも多くの人は)タイプの「オーバーフローの行動のいくつかの私がすべてではないのと同様に行うのたっぷり持っている(例えばtype.MaxValue + type.SmallestValue == type.MinValue例: int.MaxValue + 1 == int.MinValue)。

しかし、私の悪質な性質を見て、この動作を拡張して、この傷害にいくつかの侮辱を加えますDateTime。たとえば、オーバーライドされたタイプとしましょう。(私DateTimeは.NETで封印されていることを知っていますが、この例では、DateTimeが封印されていないことを除いて、C#とまったく同じ擬似言語を使用しています)。

オーバーライドされたAddメソッド:

/// <summary>
/// Increments this date with a timespan, but loops when
/// the maximum value for datetime is exceeded.
/// </summary>
/// <param name="ts">The timespan to (try to) add</param>
/// <returns>The Date, incremented with the given timespan. 
/// If DateTime.MaxValue is exceeded, the sum wil 'overflow' and 
/// continue from DateTime.MinValue. 
/// </returns>
public DateTime override Add(TimeSpan ts) 
{
    try
    {                
        return base.Add(ts);
    }
    catch (ArgumentOutOfRangeException nb)
    {
        // calculate how much the MaxValue is exceeded
        // regular program flow
        TimeSpan saldo = ts - (base.MaxValue - this);
        return DateTime.MinValue.Add(saldo)                         
    }
    catch(Exception anyOther) 
    {
        // 'real' exception handling.
    }
}

もちろん、ifはこれを簡単に解決できますが、例外を使用できない理由はわかりません(論理的には、パフォーマンスが問題である場合、例外を回避する必要があることがわかります)。 )。

多くの場合、それらはif構造よりも明確であり、メソッドが行っているコントラクトを壊すことはありません。

「通常のプログラムフローではそれらを使用しないでください」という私見は、誰もが持っていると思われる反応は、その反応の強さが正当化できるほど十分に構築されているわけではありません。

それとも私は間違っていますか?

私は他の投稿を読んで、あらゆる種類の特別なケースを扱っていますが、両方の人がいれば、何も問題はありません。

  1. 晴れ
  2. メソッドの契約を尊重する

私を撃つ。


2
+1私も同じように感じています。パフォーマンスに加えて、制御フローの例外を回避する唯一の正当な理由は、呼び出し元のコードが戻り値ではるかに読みやすくなる場合です。

4
は:何かが起こった場合は-1を返し、何か他の場合は-2を返します...例外よりも読みやすいですか?
ケンダー

1
真実を伝えることに対して否定的な評判が得られるのは悲しいことです。あなたの例がifステートメントで書かれていない可能性があるということです。(これが正しい/完全であると言っているわけではありません。)
Ingo

8
例外をスローすることが唯一の選択肢である場合があると私は主張します。たとえば、データベースにクエリを実行して、コンストラクター内の内部状態を初期化するビジネスコンポーネントがあります。データベースに適切なデータがない場合があります。コンストラクター内で例外をスローすることが、オブジェクトの構築を効果的にキャンセルする唯一の方法です。これはクラスのコントラクト(私の場合はJavadoc)に明記されているので、クライアントコードがコンポーネントの作成時に例外をキャッチしてそこから続行できることは問題ありません。
Stefan Haberl 2013

1
あなたは仮説を立てたので、裏付けとなる証拠/理由を引用する責任もあなたにあります。手始めに、コードがはるかに短い自己文書化ステートメントより優れている理由の1つを挙げてifください。これは非常に難しいでしょう。言い換えると、あなたの前提には欠陥があり、そこから引き出す結論は間違っています。
Konrad Rudolph

回答:


169

通常の操作で毎秒5つの例外を発生させるプログラムをデバッグしようとしたことがありますか?

私が持っています。

プログラムは非常に複雑で(分散計算サーバーでした)、プログラムの片側にわずかな変更を加えるだけで、まったく別の場所で何かが壊れる可能性があります。

プログラムを起動して例外が発生するのを待つだけでよかったのですが、通常の操作では、起動時に約200の例外が発生しました

私のポイント:通常の状況で例外を使用する場合、どのようにして異常な(つまり、例外的な)状況を見つけるのですか?

もちろん、例外を使いすぎない、特にパフォーマンスに関しては、他にも大きな理由があります。


13
例:.netプログラムをデバッグするとき、それをVisual Studioから起動し、VSにすべての例外を解除するように依頼します。期待される動作として例外に依存している場合、私はそれを行うことはできません(5秒/秒で中断するため)。コードの問題のある部分を見つけるのははるかに複雑です。
ブラン

15
+1は、実際の例外的な針を見つけるための例外的な干し草を作成したくないことを示します。
グラントワーグナー

16
この答えはまったくわかりません。人々はここで誤解していると思います。デバッグとはまったく関係ありませんが、デザインに関係しています。これは、私が恐れている純粋な形での循環的な推論です。そして、あなたのポイントは、前に述べたような質問の外にあります
ピーター

11
@Peter:例外を壊さずにデバッグすることは困難であり、すべての例外が意図的に多くある場合、すべての例外をキャッチすることは困難です。デバッグを困難にする設計は、ほぼ部分的に壊れていると思います(言い換えれば、設計はデバッグ、IMOと何か関係がある)
Brann

7
デバッグしたいほとんどの状況がスローされている例外に対応していないという事実を無視しても、あなたの質問への答えは「タイプ別」です。たとえば、AssertionErrorやStandardErrorだけをキャッチするようにデバッガーに指示します。起こっている悪いことに対応します。それで問題が発生した場合は、どのようにログを記録しますか?レベルとクラスごとにログを記録しないので、正確にフィルタリングできますか?それも悪い考えだと思いますか?
Ken

157

例外は基本的に非ローカルgotoステートメントであり、後者のすべての結果を伴います。フロー制御に例外を使用すると、驚きを最小限に抑えるという原則に違反するため、プログラムを読みにくくします(プログラムは最初にプログラマー向けに書かれていることに注意してください)。

さらに、これはコンパイラベンダーが期待するものではありません。彼らは例外がスローされることはまれであることを期待しており、通常はthrowコードを非常に非効率的にしています。例外のスローは、.NETで最もコストのかかる操作の1つです。

ただし、一部の言語(特にPython)では、フロー制御構成として例外を使用します。たとえば、StopIterationそれ以上アイテムがない場合、イテレータは例外を発生させます。標準言語の構成要素(などfor)でもこれに依存しています。


11
ねえ、例外は驚くべきことではありません!そして、あなたは「それは悪い考えだ」と言って、そして「Pythonではそれは良い考えだ」と言い続けるとき、あなた自身とちょっと矛盾します。
Hasenは2009

6
私はまだまったく確信が持てません:1)効率性は質問以外にもありました、多くの非バッチプログラムはそれほど気になりませんでした(例:ユーザーインターフェース)2)驚異的:私が言ったように、それはそれが使用されていないという単なる驚異的な理由ですが、問題は残っています:そもそもidを使用しないのはなぜですか?しかし、これが答えなので
ピーター

4
+1実際、PythonとC#の違いを指摘していただき、ありがとうございます。それは矛盾ではないと思います。Pythonははるかに動的であり、この方法で例外を使用することの期待は言語に組み込まれています。また、PythonのEAFPカルチャーの一部でもあります。どのアプローチが概念的に純粋であるか、より一貫性があるかはわかりませんが、他の人が期待することを実行するコードを書くというアイデアは好きです。つまり、異なる言語で異なるスタイルを意味します。
Mark E. Haase 2013

13
gotoもちろん、とは異なり、例外は呼び出しスタックや字句スコープと正しく相互作用し、スタックやスコープを混乱させないでください。
Lukas Eder

4
実際、ほとんどのVMベンダーは例外を予期し、それらを効率的に処理します。@LukasEderが指摘しているように、例外はgotoとは異なり、構造化されています。
Marcin

29

私の経験則は:

  • エラーから回復するために何かできる場合は、例外をキャッチします
  • エラーが非常に一般的なものである場合(たとえば、ユーザーが間違ったパスワードでログインしようとした場合)、returnvaluesを使用します
  • エラーから回復するために何もできない場合は、キャッチせずにそのままにしておきます(または、メインキャッチャーでキャッチして、アプリケーションをある程度正常にシャットダウンします)。

私が例外として目にする問題は、純粋に構文の観点からです(パフォーマンスのオーバーヘッドが最小限であると確信しています)。私はいたるところにtry-blocksが好きではありません。

この例を見てみましょう:

try
{
   DoSomeMethod();  //Can throw Exception1
   DoSomeOtherMethod();  //Can throw Exception1 and Exception2
}
catch(Exception1)
{
   //Okay something messed up, but is it SomeMethod or SomeOtherMethod?
}

..別の例として、ファクトリを使用してハンドルに何かを割り当てる必要があり、そのファクトリが例外をスローする場合があります。

Class1 myInstance;
try
{
   myInstance = Class1Factory.Build();
}
catch(SomeException)
{
   // Couldn't instantiate class, do something else..
}
myInstance.BestMethodEver();   // Will throw a compile-time error, saying that myInstance is uninitalized, which it potentially is.. :(

個人的には、まれなエラー条件(メモリ不足など)の例外を保持し、代わりに戻り値(値クラス、構造体、または列挙型)を使用してエラーチェックを行うべきだと思います。

私はあなたの質問が正しいことを理解したことを願っています:)


4
re:2番目の例-ビルド後、tryブロック内にBestMethodEverへの呼び出しを配置するだけではどうですか。Build()が例外をスローした場合、例外は実行されず、コンパイラーは満足しています。
Blorgbeardは、

2
うん、たぶんそれで終わりますが、myInstance-type自体が例外をスローできるより複雑な例を考えてみてください。そして、メソッドスコープ内の他のインスタンスもできます。ネストされた多くのtry / catchブロックが作成されます:(
cwap '08

あなたはあなたのcatchブロックで(抽象化のレベルに適した例外タイプへの)例外変換を行うべきです。FYI:「マルチキャッチは、」Javaの7に行くことになっている
jasonnerothin

参考:C ++では、さまざまな例外をキャッチしようとした後に複数のキャッチを配置できます。
RobH 2009

2
シュリンクラップソフトウェアの場合、すべての例外をキャッチする必要があります。少なくとも、プログラムをシャットダウンする必要があることを説明するダイアログボックスを表示します。ここに、バグレポートで送信できるわかりにくいものがあります。
David Thornley、

25

多くの答えに対する最初の反応:

あなたはプログラマーのために書いていて、驚くほどの原則はありません

もちろん!しかし、ifは常に明確ではありません。

それは驚くべきことではありません。例:分割(1 / x)キャッチ(divisionByZero)は、私(コンラッドなど)の場合よりも明確です。この種のプログラミングが期待されていないという事実は、純粋に従来型であり、実際、依然として関連性があります。たぶん私の例ではifの方がわかりやすいでしょう。

しかし、DivisionByZeroとFileNotFoundの方がifsよりも明確です。

もちろん、パフォーマンスが低く、毎秒何億回も必要な場合は、もちろんそれを回避する必要がありますが、それでも、全体的な設計を回避する十分な理由はありません。

最小の驚きの原則に関する限り、ここでは循環的な推論の危険があります。コミュニティ全体が悪いデザインを使用しているとすると、このデザインは期待されるでしょう!したがって、この原則は目標にすることはできず、慎重に検討する必要があります。

正常な状況の例外、異常な(つまり、例外的な)状況をどのように見つけますか?

多くの反応でsth。このように谷が輝いています。捕まえてください あなたの方法は明確で、十分に文書化されていて、契約である必要があります。私が認めなければならないその質問は得られません。

すべての例外のデバッグ:同じです。例外を使用しない設計が一般的であるため、時々行われます。私の質問は:なぜそもそもなぜそれが一般的であるのですか?


1
1)x電話をかける前に必ず確認します1/xか?2)すべての除算演算をtry-catchブロックにラップしてキャッチしDivideByZeroExceptionますか?3)回復するために、catchブロックにどのロジックを入れDivideByZeroExceptionますか?
ライトマン

1
DivisionByZeroとFileNotFoundは例外として扱う必要のある例外的なケースであるため、悪い例です。
0x6C38

ここにいる「反例外」の人々がうんざりしているのと同じように、ファイルが見つからないことについて「過度に例外的」なものはありません。openConfigFile();キャッチされたFileNotFoundが続き、{ createDefaultConfigFile(); setFirstAppRun(); }FileNotFound例外が正常に処理されます。クラッシュは発生しません。エンドユーザーのエクスペリエンスを向上させましょう。「しかし、これが本当に最初の実行ではなく、毎回それが得られた場合はどうなりますか?」少なくともアプリは毎回実行され、すべてのスタートアップをクラッシュさせるわけではありません!1対10の「これはひどい」の場合:起動ごとに「初回実行」= 3または4、起動ごとにクラッシュ = 10
Loduwijk

あなたの例は例外です。いいえ、通常は問題ないため、をx呼び出す前に常に確認するわけではありません1/x。例外的なケースは、それがうまくいかないケースです。ここでは地球を破壊することについて話していませんが、たとえば、ランダムなが与えられた基本的な整数の場合x、4294967296の1つだけが除算に失敗します。それは例外的であり、例外はそれに対処する良い方法です。ただし、例外を使用して同等のswitchステートメントを実装することもできますが、これはかなり愚かなことです。
Thor84no 2018年

16

例外の前に、Cではsetjmplongjmpそれがスタックフレームの同様の展開を達成するために使用することができます。

次に、同じ構成に「例外」という名前が付けられました。また、ほとんどの回答は、この名前の意味に基づいてその使用法について議論し、例外は例外的な状況で使用されることを意図していると主張しています。それは元々の意図ではありませんでしたlongjmp。多くのスタックフレームで制御フローを中断する必要がある状況がありました。

例外は、同じスタックフレーム内でも使用できるという点で少し一般的です。これはgoto私が間違っていると私が信じていることとの類似点を提起します。GOTOSは、密結合対である(そのためであるsetjmplongjmp)。例外は、よりクリーンな疎結合されたパブリッシュ/サブスクライブに従います!したがって、同じスタックフレーム内でそれらを使用することは、gotos を使用することとほとんど同じではありません。

混乱の3番目の原因は、それらがチェックされている例外かチェックされていない例外かに関するものです。もちろん、チェックされていない例外は、制御フローや他の多くのものに使用すると特にひどいようです。

ただし、チェックされた例外は、ビクトリア朝のハングアップをすべて乗り越えて少し暮らすと、制御フローには最適です。

私のお気に入りの使用法は、throw new Success()コードの長いフラグメントのシーケンスで、探しているものが見つかるまで次々に試行します。それぞれ-ロジックの各部分-は入れ子になっている可能性があるためbreak、あらゆる種類の条件テストとしても使用できます。if-elseパターンは脆いです。私が編集した場合else他の方法で構文たり混乱させたりすると、毛深いバグがあります。

を使用すると、コードフローがthrow new Success() 線形化されます。私はローカルで定義されたSuccessクラスを使用します-もちろんチェックされています-そのため、それをキャッチするのを忘れた場合、コードはコンパイルされません。そして、私は別のメソッドのSuccessesをキャッチしません。

時々、私のコードは次々にチェックし、すべてが正常な場合にのみ成功します。この場合、を使用して同様の線形化を行っていthrow new Failure()ます。

別の関数を使用すると、区画化の自然なレベルが台無しになります。したがって、returnソリューションは最適ではありません。認知上の理由から、1か所に1ページまたは2ページのコードを配置することを好みます。私は超微細に分割されたコードを信じていません。

ホットスポットがない限り、JVMやコンパイラーが何をするかは私にとってあまり重要ではありません。コンパイラーがローカルでスローされキャッチされた例外を検出せず、単純にそれらをgotoマシンコードレベルで非常に効率的なs として扱う根本的な理由があるとは信じられません。

制御フローの関数間でそれらを使用する限り(つまり、例外的なケースではなく一般的なケース)、複数のブレーク、条件テスト、3つのスタックフレームを介してウェイドに戻るよりも効率が悪くなるだけではありません。スタックポインタ。

個人的にはスタックフレーム全体でパターンを使用していませんが、エレガントにデザインを洗練する必要があることを理解できます。しかし、控えめに使用しても問題ありません。

最後に、意外な処女プログラマに関して、それは説得力のある理由ではありません。あなたが優しく彼らを実践に紹介すれば、彼らはそれを愛することを学びます。Cプログラマーを驚かせ怖がらせていたC ++を覚えています。


3
このパターンを使用すると、ほとんどの粗い関数の最後に2つの小さなキャッチがあります。1つは成功用で、もう1つは失敗用で、関数は正しいサーブレット応答の準備や戻り値の準備などをラップします。後処理を1か所で行うのは良いことです。return-pattern代替は、すべてのそのような機能のための2つの機能を必要とします。サーブレットの応答またはその他のそのようなアクションを準備するための外部の1つと、計算を行うための内部の1つ。PS:英語の教授はおそらく、最後のパラグラフで「意外」ではなく「驚くべき」を使用することを勧めるでしょう:-)
ネクロマンサー

11

標準のanwserは、例外は定期的ではなく、例外的な場合に使用する必要があるということです。

私にとって重要な理由の1つは、try-catch保守またはデバッグするソフトウェアで制御構造を読み取るときに、元のコーダーが例外処理ではなく例外処理を使用した理由を見つけようとすることです。if-else構造です。そして、私は良い答えを見つけることを期待しています。

コンピューターだけでなく他のコーダーのためにもコードを書くことを忘れないでください。例外ハンドラに関連付けられているセマンティクスがあり、マシンが気にしないからといって捨てることはできません。


過小評価されている答えだと思います。例外が飲み込まれていることを検出しても、コンピューターの速度はそれほど遅くないかもしれませんが、他の誰かのコードで作業しているときに遭遇した場合、重要なことを逃したときに、ワークアウト中に死んでしまいます。わからない、またはこのアンチパターンを使用する正当な理由が実際にないかどうか。
Tim Abell、2016年

9

パフォーマンスはどうですか?.NET Webアプリの負荷テスト中に、一般的に発生する例外を修正してその数が500ユーザーに増えるまで、Webサーバーあたりのシミュレーションユーザー数を100に増やしました。


8

Josh Blochは、Effective Javaでこのトピックを幅広く扱っています。彼の提案は明快であり、(詳細を除いて).Netにも適用する必要があります。

特に、例外は例外的な状況で使用する必要があります。この理由は、主にユーザビリティに関連しています。特定のメソッドを最大限に使用するには、その入力条件と出力条件を最大限に制約する必要があります。

たとえば、2番目の方法は最初の方法よりも使いやすいです。

/**
 * Adds two positive numbers.
 *
 * @param addend1 greater than zero
 * @param addend2 greater than zero
 * @throws AdditionException if addend1 or addend2 is less than or equal to zero
 */
int addPositiveNumbers(int addend1, int addend2) throws AdditionException{
  if( addend1 <= 0 ){
     throw new AdditionException("addend1 is <= 0");
  }
  else if( addend2 <= 0 ){
     throw new AdditionException("addend2 is <= 0");
  }
  return addend1 + addend2;
}

/**
 * Adds two positive numbers.
 *
 * @param addend1 greater than zero
 * @param addend2 greater than zero
 */
public int addPositiveNumbers(int addend1, int addend2) {
  if( addend1 <= 0 ){
     throw new IllegalArgumentException("addend1 is <= 0");
  }
  else if( addend2 <= 0 ){
     throw new IllegalArgumentException("addend2 is <= 0");
  }
  return addend1 + addend2;
}

どちらの場合も、呼び出し元がAPIを適切に使用していることを確認する必要があります。しかし、2番目のケースでは、それを(暗黙的に)必要とします。ユーザーがjavadocを読んでいない場合でも、ソフト例外はスローされますが、

  1. 文書化する必要はありません。
  2. あなたはそれをテストする必要はありません(あなたのユニットテスト戦略がどれほど積極的であるかによる)。
  3. 呼び出し元が3つのユースケースを処理する必要はありません。

基本的なポイントは、例外はリターンコードとして使用しないことです。これは、主にAPIだけでなく、呼び出し元のAPIも複雑にしたためです。

もちろん、正しいことを行うにはコストがかかります。コストは、ドキュメントを読んで従う必要があることを誰もが理解する必要があることです。うまくいけば、とにかくそうです。


7

フロー制御には例外を利用できると思います。ただし、この手法には裏返しがあります。例外の作成は、スタックトレースを作成する必要があるため、コストがかかります。したがって、例外的な状況を通知するだけの場合よりも頻繁に例外を使用する場合は、スタックトレースの構築がパフォーマンスに悪影響を及ぼさないようにする必要があります。

例外を作成するコストを削減する最良の方法は、次のようにfillInStackTrace()メソッドをオーバーライドすることです。

public Throwable fillInStackTrace() { return this; }

このような例外では、スタックトレースは入力されません。


スタックトレースでは、呼び出し元がスタック内のすべてのThrowableを "認識"(依存している)する必要もあります。これは悪いことです。抽象化のレベルに適した例外(サービスのServiceExceptions、DaoメソッドからのDaoExceptionsなど)をスローします。必要に応じて翻訳してください。
jasonnerothin 2009

5

引用したコードでプログラムの流れをどのように制御しているのか、実際にはわかりません。ArgumentOutOfRange例外以外の例外は表示されません。(したがって、2番目のcatch句はヒットしません)。あなたがしていることは、非常にコストのかかるスローを使用してifステートメントを模倣することです。

また、フロー制御を実行するためにどこか他の場所でキャッチされるように純粋に例外をスローするという、より不吉な操作を実行していません。あなたは実際には例外的なケースを扱っています。


5

ブログ投稿で説明したベストプラクティスは次のとおりです。

  • 例外をスローして、ソフトウェアの予期しない状況を示します。
  • 入力検証には戻り値を使用します
  • ライブラリがスローする例外を処理する方法を知っている場合は、可能な限り低いレベルでキャッチしてください
  • 予期しない例外が発生した場合は、現在の操作を完全に破棄してください。それらに対処する方法を知っているふりをしないでください

4

コードが読みにくいため、デバッグに問題が発生する可能性があります。長い時間をかけてバグを修正すると、新しいバグが発生します。リソースと時間の点でコストが高くなり、コードをデバッグしている場合に問題が発生します。デバッガーは例外が発生するたびに停止します;)


4

上記の理由とは別に、フロー制御に例外を使用しない理由の1つは、デバッグプロセスが非常に複雑になる可能性があることです。

たとえば、VSのバグを追跡する場合、通常は「すべての例外で中断」をオンにします。フロー制御に例外を使用している場合、私は定期的にデバッガーを中断し、実際の問題に到達するまでこれらの例外的でない例外を無視し続ける必要があります。これは誰かを怒らせる可能性があります!!


1
私はすでにそれを1つ大きくしています:すべての例外のデバッグ:同じです、例外を使用しない設計が一般的であるため、それは単に行われます。私の質問は:なぜそもそもなぜそれが一般的であるのですか?
ピーター

それであなたの答えは基本的に「Visual Studioにこの1つの機能があるので悪いことです...」ですか?私は今から約20年間プログラミングを行っており、「すべての例外で中断」オプションがあることに気づきませんでした。それでも、「この1つの機能のため」弱い理由のように聞こえます。例外をそのソースまでたどるだけです。うまくいけば、これを容易にする言語を使用しています-そうでなければ、問題は言語機能にあり、例外自体の一般的な使用法にありません。
Loduwijk 2017

3

いくつかの計算を行うメソッドがあるとしましょう。検証しなければならない多くの入力パラメーターがあり、0より大きい数値を返します。

戻り値を使用して検証エラーを通知するのは簡単です。メソッドが0未満の数値を返した場合、エラーが発生しました。その後、伝える方法どのパラメータが検証されませんでしたか?

C日から、多くの関数が次のようなエラーコードを返したことを覚えています。

-1 - x lesser then MinX
-2 - x greater then MaxX
-3 - y lesser then MinY

例外をスローしてキャッチするよりも読みにくくなりますか?


それが彼らが列挙型を発明した理由です:)しかし、魔法の数字は完全に別のトピックです..en.wikipedia.org/wiki
Isak Savo

素晴らしい例です。同じことを書こうとしていた。@IsakSavo:メソッドが意味のある値またはオブジェクトを返すことが期待される場合、列挙型はこの状況では役に立ちません。たとえば、getAccountBalance()は、AccountBalanceResultEnumオブジェクトではなくMoneyオブジェクトを返す必要があります。多くのCプログラムには同様のパターンがあり、1つのセンチネル値(0またはnull)がエラーを表し、別の関数を呼び出して別のエラーコードを取得し、エラーが発生した理由を特定する必要があります。(MySQL C APIはこのようなものです。)
Mark E. Haase 2013

3

制御フローの例外を使用できるのと同じように、ハンマーの爪を使用してねじを回すことができます。これは、機能の使用目的であることを意味するものではありません。このifステートメントは、意図された使用法フローの制御である条件を表します。

その目的のために設計された機能を使用しないことを選択しながら、意図しない方法で機能を使用している場合、関連するコストが発生します。この場合、真の付加価値がないため、明瞭さとパフォーマンスが低下します。例外を使用すると、広く受け入れられているif声明に対して何が得られますか?

別の言い方をすると、あなたができるからといって、そうすべきだという意味ではありません。


1
if通常の使用のために取得した後、またはexecptionsの使用が意図されていないために意図されていない(循環引数)後、例外は必要ないと言っていますか?
Val

1
@Val:例外は例外的な状況用です-例外をスローして処理するのに十分なことが検出された場合、例外をスローせずに処理するのに十分な情報があります。処理ロジックに直接進み、高価で余分なtry / catchをスキップできます。
ブライアンワッツ

そのロジックによって、例外がなくても、例外をスローする代わりに常にシステムを終了することができます。終了する前に何かしたい場合は、ラッパーを作成して呼び出します。Javaの例:public class ExitHelper{ public static void cleanExit() { cleanup(); System.exit(1); } }次に、スローする代わりにそれを呼び出しExitHelper.cleanExit();ます。引数が適切である場合、これが推奨されるアプローチであり、例外はありません。あなたは基本的に「例外の唯一の理由は別の方法でクラッシュすることです」と言っています。
Loduwijk 2017

@Aaron:例外のスローとキャッチの両方を行う場合、そうすることを避けるのに十分な情報があります。すべての例外が突然致命的となるわけではありません。私が制御しない他のコードがそれをキャッチするかもしれません、そしてそれは問題ありません。健全なままである私の議論は、同じコンテキストで例外をスローしてキャッチすることは不必要であるということです。すべての例外がプロセスを終了する必要があるとは述べていません。
ブライアンワッツ

@BryanWatts承認しました。他の多くの人、回復できないものに対してのみ例外を使用すべきであり、拡張により常に例外でクラッシュするはずであると述べています。これらのことを議論するのが難しいのはこのためです。2つの意見だけでなく、多くの意見があります。私はまだあなたに同意しませんが、強くはしません。throw / catchが最も読みやすく、保守しやすく、見栄えのよいコードである場合があります。通常、これはすでに他の例外をキャッチしていて、すでにtry / catchがあり、さらに1つまたは2つのキャッチを追加する方が、による個別のエラーチェックよりもクリーンな場合に発生しますif
Loduwijk

3

他の人が何度も言及しているように、最小の驚きの原則は、制御フローのみの目的で例外を過度に使用することを禁止します。一方、100%正しいルールはありません。例外が「まさに適切なツール」であるケースは常にあります- gotoちなみに、それ自体はJavaのような形式でbreakcontinueJavaのような言語で出荷されます。多くの場合、頻繁に入れ子になっているループからジャンプするのに最適な方法ですが、常に回避できるわけではありません。

次のブログ投稿では、非ローカルの かなり複雑でありながら興味深いユースケースについて説明していますControlFlowException

jOOQ(JavaのSQL抽象化ライブラリ)の内部を説明します、このような例外を使用して、「まれな」条件が満たされたときにSQLレンダリングプロセスを早期に中止。

そのような条件の例は次のとおりです。

  • バインド値が多すぎます。一部のデータベースは、SQLステートメントで任意の数のバインド値をサポートしていません(SQLite:999、Ingres 10.1.0:1024、Sybase ASE 15.5:2000、SQL Server 2008:2100)。このような場合、jOOQはSQLレンダリングフェーズを中止し、インラインバインド値を使用してSQLステートメントを再レンダリングします。例:

    // Pseudo-code attaching a "handler" that will
    // abort query rendering once the maximum number
    // of bind values was exceeded:
    context.attachBindValueCounter();
    String sql;
    try {
    
      // In most cases, this will succeed:
      sql = query.render();
    }
    catch (ReRenderWithInlinedVariables e) {
      sql = query.renderWithInlinedBindValues();
    }

    クエリASTからバインド値を明示的に抽出して毎回カウントする場合、この問題の影響を受けないクエリの99.9%で貴重なCPUサイクルが無駄になります。

  • 一部のロジックは、「部分的に」のみ実行したいAPIを介して間接的にのみ使用できます。UpdatableRecord.store()この方法は、生成INSERTまたはUPDATEに応じて、声明をRecordの内部フラグ。「外部」からは、どのようなロジックが含まれているのかstore()(たとえば、オプティミスティックロック、イベントリスナーの処理など)がわからないため、バッチステートメントに複数のレコードを格納するときにそのロジックを繰り返したくありません。ここではstore()、SQLステートメントを生成するだけで、実際には実行しないようにします。例:

    // Pseudo-code attaching a "handler" that will
    // prevent query execution and throw exceptions
    // instead:
    context.attachQueryCollector();
    
    // Collect the SQL for every store operation
    for (int i = 0; i < records.length; i++) {
      try {
        records[i].store();
      }
    
      // The attached handler will result in this
      // exception being thrown rather than actually
      // storing records to the database
      catch (QueryCollectorException e) {
    
        // The exception is thrown after the rendered
        // SQL statement is available
        queries.add(e.query());                
      }
    }

    store()ロジックを「再利用可能な」APIに外部化し、オプションでSQLを実行しないようにカスタマイズできる場合、保守が難しく、再利用が困難なAPIの作成を検討しています。

結論

本質的に、これらの非ローカルgotosの使用法は、[メイソンウィーラー] [5]が彼の回答で述べたことに沿っています。

「私はそれを処理するための十分なコンテキストがないため、この時点で適切に処理できない状況に遭遇しましたが、私を呼び出すルーチン(またはコールスタックのさらに上にあるもの)は、それを処理する方法を知っている必要があります」

の両方の使用法はControlFlowExceptions、代替案に比べて実装が簡単で、関連する内部からリファクタリングすることなく、幅広いロジックを再利用できます。

しかし、これが将来のメンテナにとってはちょっと驚きだという気持ちは残っています。コードはかなり繊細に感じられ、この場合は正しい選択でしたが、通常の分岐を使用することを簡単に回避できるローカル制御フローには例外を使用しないことを常に好みif - elseます。


2

通常、それ自体は問題ではなく、低レベルで例外を処理します。例外は、操作を実行できない理由について多くの詳細を提供する有効なメッセージです。そして、あなたがそれを扱うことができるなら、あなたはすべきです。

一般に、チェックできる失敗の可能性が高いことがわかっている場合は、チェックを行う必要があります... if(obj!= null)obj.method()

あなたの場合、私はC#ライブラリに慣れていないので、日付時刻がタイムスタンプが範囲外であるかどうかを確認する簡単な方法があるかどうかを知ることができません。含まれている場合は、if(.isvalid(ts))を呼び出します。そうでない場合、コードは基本的に問題ありません。

したがって、基本的には、どちらの方法でもよりクリーンなコードを作成することになります...予期される例外から保護するための操作が、単に例外を処理するよりも複雑な場合。どこにでも複雑な警備員を作成する代わりに、あなたが例外を処理する私の許可を持っているよりも。


追加ポイント:例外が障害キャプチャー情報(「Param getWhatParamMessedMeUp()」のようなゲッター)を提供する場合、それはAPIのユーザーが次に何をすべきかについての正しい決定を下すのに役立ちます。それ以外の場合は、エラー状態に名前を付けるだけです。
jasonnerothin 2009

2

制御フローに例外ハンドラーを使用している場合、一般的すぎて面倒です。他の誰かが述べたように、ハンドラーで処理を処理しているときに何かが起こったことを知っていますが、正確には何ですか?制御フローに使用している場合は、基本的に、elseステートメントの例外を使用しています。

起こりうる状態がわからない場合は、予期しない状態の例外ハンドラーを使用できます。たとえば、サードパーティライブラリを使用する必要がある場合や、UIですべてをキャッチして素敵なエラーを表示する必要がある場合などです。メッセージを送信し、例外をログに記録します。

ただし、問題が発生する可能性があることがわかっていて、ifステートメントや何かをチェックしない場合は、怠惰です。例外ハンドラーが発生する可能性のあるもののキャッチオールになることを許可するのは怠惰であり、おそらく誤った仮定に基づいて例外ハンドラーの状況を修正しようとするため、後で悩まされることになります。

何が起こったのかを正確に判断するために例外ハンドラーにロジックを配置すると、そのロジックをtryブロック内に配置しないことは非常に愚かになります。

例外ハンドラーは、何かがうまくいかないようにするためのアイデアや方法が足りなくなった場合や、物事が自分の制御能力を超えている場合の最後の手段です。同様に、サーバーがダウンしてタイムアウトになり、その例外がスローされるのを防ぐことはできません。

最後に、すべてのチェックを前もって行うことで、発生することがわかっているまたは予想されることがわかり、それが明示的になります。コードは意図を明確にする必要があります。どちらを読みますか?


1
まったく当てはまらない:「本質的に、制御フローに使用している場合、elseステートメントの例外を使用しています。」制御フローに使用している場合、正確に何をキャッチしているかがわかり、一般的なキャッチは使用しませんが、もちろん特定のもの!
ピーター

2

正しく行われた例外の一種の一般化であるCommon Lispの条件システムを見ることに興味があるかもしれません。スタックを巻き戻したり、制御された方法で巻き戻したりできないため、「再起動」もできるため、非常に便利です。

これは他の言語のベストプラクティスとは何の関係もありませんが、考えている方向に(大まかに)ある設計思想で何ができるかを示しています。

もちろん、ヨーヨーのようにスタックを上下にバウンスする場合でも、パフォーマンスに関する考慮事項はありますが、ほとんどのキャッチ/スロー例外システムが具現化している「あらくだ、保釈しましょう」というアプローチよりもはるかに一般的な考え方です。


2

フロー制御に例外を使用することに問題はないと思います。例外は継続にいくらか似ており、静的に型付けされた言語では、例外は継続よりも強力であるため、継続が必要だが言語に継続がない場合は、例外を使用してそれらを実装できます。

まあ、実際には、継続が必要で、言語に継続がない場合は、間違った言語を選択し、別の言語を使用する必要があります。ただし、選択肢がない場合もあります。クライアント側のWebプログラミングがその典型的な例です。JavaScriptを回避する方法はありません。

例:Microsoft Voltaは、単純な.NETでWebアプリケーションを作成できるようにするプロジェクトであり、どのビットをどこで実行する必要があるかをフレームワークに認識させます。この結果、VoltaはCILをJavaScriptにコンパイルして、クライアントでコードを実行できるようにする必要があります。ただし、問題があります。.NETにはマルチスレッドがありますが、JavaScriptにはありません。そのため、VoltaはJavaScript例外を使用してJavaScriptに継続を実装し、それらの継続を使用して.NETスレッドを実装します。このようにして、スレッドを使用するVoltaアプリケーションをコンパイルして、変更されていないブラウザーで実行できます。Silverlightは必要ありません。


2

美的理由の1つ:

トライは常にキャッチが付いていますが、イフは他に付いている必要はありません。

if (PerformCheckSucceeded())
   DoSomething();

try / catchを使用すると、はるかに冗長になります。

try
{
   PerformCheckSucceeded();
   DoSomething();
}
catch
{
}

これは、6行のコードが多すぎます。


1

ただし、呼び出すメソッドで何が発生するかは常にわかりません。例外がスローされた場所は正確にはわかりません。例外オブジェクトを詳細に調べることなく...


1

あなたの例には何も問題はないと思います。逆に、呼び出された関数によってスローされた例外を無視するのは罪です。

JVMでは、例外のスローはそれほど高価ではなく、新しいxyzException(...)で例外を作成するだけです。後者はスタックウォークを伴うためです。したがって、事前に作成された例外がある場合、コストなしで何度も例外をスローする可能性があります。もちろん、この方法では例外と一緒にデータを渡すことはできませんが、とにかくそれは悪いことだと思います。


申し訳ありませんが、これは明らかに間違っています、ブラン。状態により異なります。状態が取るに足らないものであるとは限りません。したがって、ifステートメントには数時間、数日、またはそれ以上かかる場合があります。
Ingo

JVMでは、それはです。返品よりも高価ではありません。図を行きます。しかし、問題は、呼び出された関数にすでに存在するコードではなく、ifステートメントで何を書いて、例外的なケースを通常のケースから区別するかです。つまり、コードの重複です。
Ingo

1
Ingo:例外的な状況は、予期しないものです。つまり、プログラマーとは思わなかったものです。だから私のルールは「例外をスローしないコードを書く」です:)
Brann

1
例外ハンドラーを作成することはありません。常に問題を修正します(障害のあるコードを制御できないためにそれができない場合を除きます)。そして、私が書いたコードが他の誰かが使用することを目的としている場合(ライブラリなど)を除いて、例外をスローすることはありません。矛盾を見せてくれませんか?
ブラン

1
例外を乱暴に投げないように同意します。しかし、確かに、「例外的」なのは定義の問題です。そのString.parseDoubleは、たとえば、適切な結果を提供できない場合、例外がスローされます。他に何をすべきですか?NaNを返しますか?IEEE以外のハードウェアはどうですか?
インゴ、

1

言語が値を返さずにメソッドを終了し、次の「キャッチ」ブロックに戻るための一般的なメカニズムがいくつかあります。

  • メソッドでスタックフレームを調べて呼び出しサイトを特定し、呼び出しサイトのメタデータを使用してtry、呼び出しメソッド内のブロックに関する情報、または呼び出しメソッドが呼び出し元のアドレスを格納した場所を検索します。後者の状況では、呼び出し元の呼び出し元のメタデータを調べて、直接の呼び出し元と同じ方法で決定し、tryブロックが見つかるか、スタックが空になるまで繰り返します。このアプローチは、例外のない場合にオーバーヘッドをほとんど追加しません(一部の最適化を排除します)が、例外が発生するとコストが高くなります。

  • 通常の戻りと例外を区別する「非表示」フラグをメソッドに返し、呼び出し側がそのフラグをチェックして、フラグが設定されている場合は「例外」ルーチンに分岐するようにします。このルーチンは、例外がない場合に1〜2の命令を追加しますが、例外が発生した場合のオーバーヘッドは比較的少ないです。

  • 呼び出し元に、例外処理情報またはコードを、スタックされた戻りアドレスに対して相対的な固定アドレスに配置してもらいます。たとえば、ARMでは、「BLサブルーチン」という命令を使用する代わりに、次のシーケンスを使用できます。

        adr lr,next_instr
        b subroutine
        b handle_exception
    next_instr:
    

正常に終了するには、サブルーチンは単にbx lrorを実行しpop {pc}ます。異常終了の場合、サブルーチンは、戻り値または使用を実行する前にLRから4を減算しますsub lr,#4,pc(ARMのバリエーション、実行モードなどに応じて)。呼び出し側がそれに対応するように設計されていない場合、このアプローチは非常に誤動作します。

チェックされた例外を使用する言語またはフレームワークは、上記の#2または#3のようなメカニズムで処理されることでメリットが得られ、チェックされていない例外は#1を使用して処理されます。Javaでのチェック例外の実装は厄介ですが、呼び出しサイトが本質的に「このメソッドはXXをスローするものとして宣言されていますが、私はそれを期待していません。そうする場合は、「チェックされていない」例外として再スローします。チェックされた例外がこのような方法で処理されたフレームワークでは、一部のコンテキストでは、失敗する可能性は高いですが、失敗すると成功とは根本的に異なる情報が返されるはずです。ただし、そのようなパターンを使用するフレームワークは認識していません。代わりに、より一般的なパターンは、すべての例外に対して上記の最初のアプローチを使用することです(例外のない場合は最小のコストですが、例外がスローされる場合は高コスト)。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.