web-dev-qa-db-ja.com

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

私がグーグルにかけることができるすべての標準的な回答を避けるために、私はあなたがすべて自由に攻撃できる例を提供します。

C#とJava(および他の多すぎる)は、私がまったく好きではない「オーバーフロー」動作のいくつかのタイプを持っています(例:type.MaxValue + type.SmallestValue == type.MinValue 例えば : int.MaxValue + 1 == int.MinValue)。

しかし、私の悪質な性質を見て、この動作を拡張することにより、この傷害にsome辱を加えます。たとえば、オーバーライドされた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. メソッドの契約を尊重する

私を撃つ。

180
Peter

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

私は持っています。

プログラムは非常に複雑で(分散計算サーバーでした)、プログラムの片側をわずかに変更するだけで、まったく別の場所で何かを簡単に破ることができました。

プログラムを起動して例外が発生するのを待っていればよかったのに、通常の操作の過程で起動中に約200の例外があった

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

もちろん、例外をあまり使用しない他の強力な理由、特にパフォーマンス面では

160
Brann

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

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

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

149
Anton Gogolev

私の経験則は次のとおりです。

  • エラーから回復するために何かできる場合は、例外をキャッチします
  • エラーが非常に一般的なものである場合(たとえば、ユーザーが間違ったパスワードでログインしようとした場合)、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.. :(

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

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

26
cwap

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

あなたはプログラマと最小限の驚きの原則のために書いています

もちろん!しかし、もしそれがいつでもはっきりしているわけではありません。

astonishingであってはなりません。たとえば、div(1/x)catch(divisionByZero)は、私にとっては(Conradなどで)どのifよりも明確です。この種のプログラミングが予期されていないという事実は、純粋に従来のものであり、実際、依然として関連しています。たぶん私の例ではifがより明確になるでしょう。

しかし、DivisionByZeroとFileNotFoundはifsよりも明確です。

もちろん、パフォーマンスが低下し、1秒あたり数十億回必要な場合は、もちろんそれを避ける必要がありますが、それでも全体的な設計を避ける正当な理由を読んでいません。

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

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

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

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

22
Peter

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

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

例外は、同じスタックフレーム内でも使用できるという点で、少し一般的です。これは、gotoとの類推を間違っていると思います。 Gotoは密結合ペアです(setjmplongjmpも同様です)。例外は、緩やかに結合されたパブリッシュ/サブスクライブの後に続きます。したがって、同じスタックフレーム内でそれらを使用することは、gotosを使用することとほとんど同じです。

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

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

私のお気に入りの使用法は、探しているものが見つかるまで次々に試行するコードの長いフラグメント内のthrow new Success()のシーケンスです。各事柄-ロジックの各部分-が任意の入れ子になっている可能性があるため、break 'sもあらゆる種類の条件テストとして出力されます。 _if-else_パターンは脆弱です。 elseを編集するか、他の方法で構文を台無しにすると、毛深いバグが発生します。

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

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

別の関数を使用すると、自然なレベルの区分化が乱れます。したがって、return解は最適ではありません。認知上の理由から、1か所に1ページまたは2つのコードを配置することを好みます。私は、非常に細かく分割されたコードを信じていません。

ホットスポットがない限り、JVMまたはコンパイラが行うことは私にはあまり関係がありません。コンパイラがローカルでスローおよびキャッチされた例外を検出せず、単にマシンコードレベルで非常に効率的なgotosとして扱う基本的な理由があるとは信じられません。

制御フローの機能間でそれらを使用する限り-i。 e。例外的なケースではなく一般的なケースの場合-単にスタックポインターを復元するのではなく、複数のブレーク、条件テスト、戻り値が3つのスタックフレームを介して戻るよりも効率が悪いことがわかりません。

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

最後に、驚くべき初心者プログラマーに関しては、説得力のある理由ではありません。あなたがそっと彼らに練習を紹介すると、彼らはそれを愛することを学ぶでしょう。私はC++がCプログラマーを驚かせ、怖がらせていたのを覚えています。

15
necromancer

標準のアンサーでは、例外は規則的ではないため、例外的な場合に使用する必要があります。

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

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

11
mouviciel

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

8
James Koch

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つのユースケースを処理する必要はありません。

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

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

8
jasonnerothin

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

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

public Throwable fillInStackTrace() { return this; }

このような例外には、スタックトレースが書き込まれません。

7
paweloque

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

また、フロー制御を実行するために例外をどこかでキャッチするためだけに例外をスローするという、より厄介な操作を実行していません。実際に例外的なケースを処理しています。

5
Jason Punyon

述べられている理由とは別に、フロー制御に例外を使用しない1つの理由は、デバッグプロセスが大幅に複雑になる可能性があることです。

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

4
Sean

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

4
Gambrinus

ブログ投稿 で説明したベストプラクティスを次に示します。

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

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

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

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

3
Bryan Watts

いくつかの計算を行うメソッドがあると仮定しましょう。検証してから0より大きい数値を返す必要がある入力パラメーターは多数あります。

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

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

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

等.

それは本当に例外を投げてキャッチするよりも読みにくいですか?

3
kender

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

これは他の言語のベストプラクティスとは何の関係もありませんが、(おおよそ)考えている方向でいくつかのデザイン思考で何ができるかを示しています。

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

2
simon

審美的な理由:

Tryには常にcatchが付属しますが、ifにはelseが付属する必要はありません。

if (PerformCheckSucceeded())
   DoSomething();

Try/catchでは、さらに冗長になります。

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

6行のコードが多すぎます。

2
gzak

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

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

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

2
Jörg W Mittag

通常、例外を低レベルで処理すること自体は問題ありません。例外IS操作を実行できない理由について多くの詳細を提供する有効なメッセージです。処理できる場合は、実行する必要があります。

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

あなたの場合、タイムスタンプが範囲外であるかどうかを確認する簡単な方法が日付時刻にあるかどうかを知るために、C#ライブラリに十分に精通していません。もしそうなら、単にif(.isvalid(ts))を呼び出してください。そうでなければ、あなたのコードは基本的には問題ありません。

したがって、基本的には、どちらの方法できれいなコードを作成するかになります...予想される例外を防ぐための操作が、例外を処理するよりも複雑な場合。どこにでも複雑なガードを作成するのではなく、例外を処理する許可を持っているよりも。

2
Patrick

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

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

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

  • 呼び出し元に、例外処理情報またはコードをスタックされたリターンアドレスに関連する固定アドレスに配置させます。たとえば、ARMでは、命令「BLサブルーチン」を使用する代わりに、シーケンスを使用できます。

        adr lr,next_instr
        b subroutine
        b handle_exception
    next_instr:
    

正常に終了するには、サブルーチンは単にbx lrまたはpop {pc};を実行します。異常終了の場合、サブルーチンはリターンを実行する前にLRから4を引くか、sub lr,#4,pcを使用します(ARMバリエーション、実行モードなどに依存))このアプローチ発信者がそれに対応するように設計されていない場合、非常にひどく誤動作します。

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

1
supercat

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

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

1
Ingo

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

1
Mesh