エラー処理を実行する最新の方法…


118

私はしばらくこの問題を熟考してきましたが、絶えず警告や矛盾を見つけているので、誰かが次の結論を出すことを望んでいます。

エラーコードよりも例外を優先する

私が知っている限りでは、業界で4年間働いて、本やブログなどを読んでから、エラーを処理するための現在のベストプラクティスは、エラーコード(必ずしもエラーコードではなく、エラーを表すタイプ)。

しかし-私にはこれは矛盾しているようです...

実装ではなく、インターフェースへのコーディング

結合を減らすために、インターフェイスまたは抽象化にコーディングします。インターフェースの特定のタイプと実装を知りませんし、知りたくありません。では、どの例外をキャッチする必要があるのか​​をどのようにして知ることができますか?実装は10種類の例外をスローすることも、何もスローしないこともあります。例外を確実にキャッチするとき、実装について仮定しているのでしょうか?

場合を除き-インターフェイスが持っています...

例外仕様

一部の言語では、開発者は特定のメソッドが特定の例外をスローすることを宣言できます(たとえば、Javaはthrowsキーワードを使用します)。

しかし-これは...を示唆しているようです

漏れやすい抽象化

インターフェイスでスローできる例外を指定する必要があるのはなぜですか?実装が例外をスローする必要がない場合、または他の例外をスローする必要がある場合はどうなりますか?インターフェイスレベルでは、実装がスローする例外を知る方法はありません。

そう...

結論する

ソフトウェアのベストプラクティスに矛盾するように見える(私の目には)のに、なぜ例外が優先されるのですか?また、エラーコードが非常に悪い場合(およびエラーコードの悪徳で販売する必要がない場合)、別の選択肢がありますか?上記のベストプラクティスの要件を満たしているが、エラーコードの戻り値をチェックする呼び出しコードに依存しない、エラー処理の現在の(またはまもなく)技術の現状は何ですか?


12
漏れやすい抽象化についての議論はありません。特定のメソッドスローする例外を指定することは、インターフェース仕様の一部です。戻り値の型がインターフェース仕様の一部であるように。例外は関数の「左側から出る」だけではありませんが、それでもインターフェースの一部です。
12

@decezeインターフェイスは、実装がスローする可能性のあるものをどのように宣言できますか?型システム内のすべての例外をスローする場合があります!そして、多くの言語が例外仕様をサポートしていないことは、それらがかなり危険であることを示唆しています
-RichK

1
メソッド自体が他のメソッドを使用し、それらの例外を内部でキャッチしない場合、メソッドがスローする可能性のあるすべての異なる例外を管理するのは難しい場合があることに同意します。とはいえ、それは矛盾ではありません。
12:36にdeceze

1
ここでの漏れやすい仮定は、コードがインターフェース境界をエスケープするときに例外を処理できるということです。実際に何が間違っていたかについてほとんど十分に知らないことを考えると、それは非常にありそうもないことです。わかっているのは、何かがうまくいかず、インターフェイスの実装がその対処方法を知らなかったことだけです。それがより良い仕事をする可能性は、それを報告してプログラムを終了する以上に貧弱です。または、インターフェイスが適切なプログラム操作に重要でない場合は無視します。インターフェイスコントラクトでエンコードできない、エンコードすべきではない実装の詳細。
ハンスパッサン

回答:


31

まず第一に、私はこの声明に同意しません。

エラーコードよりも例外を優先する

これは常に当てはまるわけではありません。たとえば、Objective-C(Foundationフレームワークを使用)を見てください。そこでは、Java開発者が@ try、@ catch、@ throw、NSExceptionクラスなどの真の例外と呼ぶものが存在するにもかかわらず、NSErrorがエラーを処理する好ましい方法です。

ただし、多くのインターフェイスは、スローされた例外で抽象化をリークするのは事実です。これは、「例外」スタイルのエラーの伝播/処理のせいではないというのが私の信念です。一般的に、エラー処理に関する最善のアドバイスは次のとおりです。

可能な限り低いレベル、期間でエラー/例外に対処する

その経験則に固執すれば、抽象化からの「漏れ」の量は非常に制限され、抑制されると思います。

で、その宣言の一部であるべきメソッドによってスローされた例外かどうか、私は、彼らが必要と考えている:彼らはの一部である契約このインタフェースで定義される:この方法ではない、またはBまたはCで失敗

たとえば、クラスがXMLパーサーである場合、その設計の一部は、提供されたXMLファイルが単なる間違っていることを示すことです。Javaでは、通常、発生する可能性のある例外を宣言し、それらをthrowsメソッドの宣言の一部に追加することでこれを行います。一方、解析アルゴリズムの1つが失敗した場合、その例外を未処理で渡す理由はありません。

それはすべて、1つのことに要約され ます。優れたインターフェイスデザインです。 インターフェイスを十分に設計すれば、例外に悩まされることはありません。さもなければ、それはあなたを悩ますただの例外ではありません。

また、Javaの作成者には、メソッドの宣言/定義の例外を含める非常に強力なセキュリティ上の理由があると思います。

最後に、Eiffelなどの一部の言語には、エラー処理のための他のメカニズムがあり、単純にスロー機能が含まれていません。そこでは、ルーチンの事後条件が満たされない場合、ソートの「例外」が自動的に発生します。


12
「エラーコードよりも例外を優先」を否定するための+1。例外は、実際には例外であり、規則ではない限り、例外は適切であると教えられてきました。条件を満たしているかどうかを判断するために例外をスローするメソッドを呼び出すことは、非常に悪い習慣です。
ニール

4
@ジョシュアドレーク:彼は間違いです。例外とgotoは非常に異なっています。たとえば、例外は常にコールスタックの同じ方向に進みます。そして第二に、不意の例外から身を守ることはDRYとまったく同じ習慣です-例えば、C ++では、RAIIを使用してクリーンアップを保証する場合、例外だけでなくすべての通常の制御フローでもクリーンアップを保証します。これは、はるかに信頼性が高くなります。try/finally似たようなことを実現します。クリーンアップを適切に保証する場合、例外を特別なケースと見なす必要はありません。
-DeadMG

4
@JoshuaDrake申し訳ありませんが、ジョエルはそこまで来ています。例外はgotoと同じではなく、常に少なくとも1レベル上の呼び出しスタックに移動します。上記のレベルでエラーコードをすぐに処理できない場合はどうしますか?さらに別のエラーコードを返しますか?エラーの問題の一部は、それらを無視できることであり、例外のスローよりも悪い問題につながります。
アンディ

25
-1。「可能な限り低いレベルでエラー/例外に対処する」ことに完全に同意しないためです-それは間違っています、期間。適切なレベルでエラー/例外に対処します。かなり頻繁にそれがはるかに高いレベルであり、その場合、エラーコードは大きな痛みです。エラーコードよりも例外を優先する理由は、それらのレベルに影響を与えることなく、どのレベルで対処するかを自由に選択できるためです。
マイケルボルグワード

2
@Giorgio:参照artima.com/intv/handcuffs.html -特にページ2と3
マイケルBorgwardt

27

エラーと代替コードパスを処理する方法は、例外とエラーコードだけではないことに注意してください。

頭の中で、Haskellのようなアプローチを使用できます。複数のコンストラクターを使用して抽象データ型を介してエラーを通知できます(識別された列挙型、またはnullポインターを考えますが、タイプセーフであり、構文を追加する可能性があります)コードフローを適切に表示するためのシュガーまたはヘルパー関数)。

func x = do
    a <- operationThatMightFail 10
    b <- operationThatMightFail 20
    c <- operationThatMightFail 30
    return (a + b + c)

operationThatMightfailは、Maybeにラップされた値を返す関数です。null可能ポインターのように機能しますが、do表記は、a、b、またはcのいずれかが失敗した場合に、全体がnullと評価されることを保証します。(そして、コンパイラは、偶発的なNullPointerExceptionの発生を防ぎます)

別の可能性は、呼び出すすべての関数に追加の引数としてエラーハンドラオブジェクトを渡すことです。このエラーハンドラーには、渡される関数によって通知される可能性のある「例外」ごとにメソッドがあり、例外によってスタックを巻き戻す必要なく、例外が発生した場合にその関数で使用して例外を処理できます。

Common LISPはこれを実行し、構文サポート(暗黙の引数)を持ち、組み込みプロトコルをこのプロトコルに準拠させることにより、実行可能にします。


1
それは本当にすてきです。代替案に関する部分に回答していただきありがとうございます:)
RichK

1
ところで、例外は現代の命令型言語の最も機能的な部分の1つです。例外はモナドを形成します。例外はパターンマッチングを使用します。例外Maybeは、実際に何を学習することなく、適用スタイルを模倣するのに役立ちます。
9000

私は最近SMLに関する本を読んでいました。オプションの種類、例外(および継続)について言及しました。アドバイスは、未定義のケースがかなり頻繁に発生すると予想される場合はオプションタイプを使用し、未定義のケースがほとんど発生しない場合は例外を使用することでした。
ジョルジオ

8

はい、例外は漏れやすい抽象化を引き起こす可能性があります。しかし、この点でエラーコードはさらに悪くありませんか?

この問題に対処する1つの方法は、どのような状況でどの例外をスローできるかをインターフェイスに正確に指定させ、必要に応じて例外をキャッチ、変換、再スローすることにより、実装が内部例外モデルをこの仕様にマップする必要があることを宣言することです。「完全な」インターフェースが必要な場合は、これが方法です。

実際には、通常、インターフェイスの論理的な一部であり、クライアントがキャッチして何かを行いたい例外を指定するだけで十分です。一般に、低レベルのエラーが発生した場合やバグが発生した場合は、他の例外が発生する可能性があり、クライアントは一般的にエラーメッセージを表示したりアプリケーションをシャットダウンしたりすることでしか処理できないことを理解しています。少なくとも例外には、問題の診断に役立つ情報を含めることができます。

実際、エラーコードでは、ほぼ同じことが暗黙のうちに発生し、情報が失われ、アプリが一貫性のない状態になる可能性がはるかに高くなります。


1
エラーコードが漏れやすい抽象化を引き起こす理由を理解していません。通常の戻り値の代わりにエラーコードが返され、この動作が関数/メソッドの仕様で説明されている場合、IMOにはリークはありません。それとも私は何かを見落としていますか?
ジョルジオ

APIが実装に依存しないはずのエラーコードが実装に固有である場合、エラーコードを指定して返すと、不要な実装の詳細が漏洩します。典型的な例:ファイルベースおよびDBベースの実装でのロギングAPI。前者には「ディスクがいっぱい」エラーがあり、後者には「DB接続がホストによって拒否されました」エラーがあります。
マイケルボルグ

何が言いたいのか理解した。実装固有のエラーを報告する場合、APIは実装に依存しません(エラーコードも例外もありません)。唯一の解決策は、「リソースが利用できない」などの実装に依存しないエラーコードを定義するか、APIが実装に依存しないことを決定することです。
ジョルジオ

1
@Giorgio:はい、実装に依存しないAPIで実装固有のエラーを報告するのは難しいです。それでも、(エラーコードとは異なり)複数のフィールドを持つことができるため、例外を使用して実行できます。したがって、例外のタイプを使用して一般的なエラー情報(ResourceMissingException)を提供し、実装固有のエラーコード/メッセージをフィールドとして含めることができます。両方の長所 :-)。
-sleske

ところで、それがjava.lang.SQLExceptionの機能です。それは持っているgetSQLState(ジェネリック)及びgetErrorCode(ベンダー固有の)。適切なサブクラスしかなかった場合
...-sleske

5

ここにはたくさんの良いものがありますが、通常の制御フローの一部として例外を使用するコードには注意が必要です。時々、人々はそのintoに陥り、通常ではないものが例外になります。ループの終了条件として使用される例外を見たことがあります。

例外とは、「ここで処理できないことが起こったため、他の人に外出して何をすべきかを判断する必要がある」という意味です。無効な入力を入力するユーザーは例外ではありません(再入力などによって入力によってローカルに処理される必要があります)。

私が見た例外使用のもう1つの退化したケースは、最初の応答が「例外を投げる」という人々です。ほとんどの場合、これはcatchを記述することなく行われます(経験則:最初にcatchを記述し、次にthrowステートメントを記述します)。大きなアプリケーションでは、キャッチされない例外がネザー領域からバブルアップし、プログラムを爆破するときに問題になります。

私は反例外ではありませんが、数年前のシングルトンのように見えます。あまりにも頻繁に、不適切に使用されています。それらは意図された使用に最適ですが、そのケースは、一部の人が考えるほど広くはありません。


4

漏れやすい抽象化

インターフェイスでスローできる例外を指定する必要があるのはなぜですか?実装が例外をスローする必要がない場合、または他の例外をスローする必要がある場合はどうなりますか?インターフェイスレベルでは、実装がスローする例外を知る方法はありません。

いや。例外仕様は、戻り値および引数のタイプと同じバケットにあります-それらはインターフェースの一部です。その仕様に準拠できない場合は、インターフェイスを実装しないでください。投げないなら大丈夫です。インターフェイスで例外を指定することについて漏れはありません。

エラーコードはひどいものです。彼らはひどいです。毎回、呼び出しごとに、それらを確認して伝播することを手動で覚えておく必要があります。これは、最初はDRYに違反し、エラー処理コードを大幅に爆破します。この繰り返しは、例外に直面するよりもはるかに大きな問題です。例外を黙って無視することはできませんが、人々は戻りコードを黙って無視することができますし、間違いなく悪いことです。


ただし、適切な形式のシュガー構文またはヘルパーメソッドがある場合、エラーコードは使いやすくなります。一部の言語では、エラーコードの処理を忘れないことをコンパイラと型システムに保証できます。例外インターフェースの部分に関しては、彼はJavaのチェック例外の悪名高い不格好さについて考えていたと思います。一見、完全に合理的なアイデアのように見えますが、実際には多くの痛みを伴う小さな問題を引き起こします。
hugomg

@missingno:それは、いつものように、Javaがそれらのひどい実装を持っているからです。チェックされた例外が本質的に悪いからではありません。
-DeadMG

1
@missingno:チェック例外によってどのような問題が発生する可能性がありますか?
ジョルジオ

@Giorgio:メソッドがスローする可能性のあるすべての種類の例外を予測するのは非常に困難です。これは、他のサブクラスとまだ記述されていないコードを考慮する必要があるためです。実際には、システム例外クラスをすべてに再利用したり、内部例外を頻繁にキャッチして再スローしたりするなど、情報を捨てるthatい回避策があります。また、言語に匿名関数を追加しようとしたときに、チェック例外が大きなハードルだと聞いたことがあります。
hugomg

@missingno:AFAIKのJavaの匿名関数は、1つのメソッドを使用した匿名内部クラスの単なる構文上のシュガーです。そのため、チェック例外が問題になる理由がわかりません(しかし、このトピックについてはあまり知りません)。はい、メソッドがどの例外をスローするかを予測することは困難です。そのため、IMOがチェック例外を持っていると便利であるため、推測する必要はありません。もちろん、貧弱なコードを記述してそれらを処理することもできますが、これは未チェックの例外でも実行できます。しかし、議論は非常に複雑であり、正直なところ、双方に賛否両論があります。
ジョルジオ

2

まあ例外処理は、独自のインターフェイス実装を持つことができます。スローされた例外のタイプに応じて、必要な手順を実行します。

設計上の問題を解決するには、2つのインターフェイス/抽象化の実装を使用します。1つは機能用で、もう1つは例外処理用です。そして、キャッチされた例外のタイプに応じて、適切な例外タイプクラスを呼び出します。

エラーコードの実装は、例外を処理する正統的な方法です。これは、文字列の使用と文字列ビルダーのようなものです。


4
つまり、実装はAPIで定義された例外のサブクラスをスローします。
アンドリュークック

2

IM-very-HO例外は、制御フローを壊すことにより、コードの実際の複雑さと知覚される複雑さを増加させるため、多くの場合不必要に、ケースバイケースで判断されます。関数内で例外をスローすることに関する議論はさておき、これは実際に制御フローを改善する可能性があります。呼び出し境界を介して例外をスローする場合は、次のことを考慮してください。

呼び出し先が制御フローを中断することを許可しても、実際の利点が得られない場合があり、例外に対処する意味のある方法がない場合があります。直接的な例として、Observableパターンを実装している場合(C#など、あらゆる場所にイベントthrowsがあり、定義に明示的なものがない言語)、クラッシュした場合にObserverが制御フローを中断できる実際の理由はありません。彼らのものに対処する意味のある方法はありません(もちろん、良い隣人は観察時に投げてはいけませんが、誰も完璧ではありません)。

上記の所見は、(指摘したように)任意の疎結合インターフェースに拡張できます。実際には、3〜6スタックフレームを忍び寄った後、キャッチされない例外が次のいずれかのコードセクションで終わる可能性が高いと思います。

  • 例外自体がアップキャストされている場合でも、意味のある方法で例外を処理するには抽象的すぎます。
  • 汎用機能を実行している(メッセージポンプやオブザーバブルなど、失敗した理由を気にする必要はありません)。
  • 具体的ですが、責任は異なります。本当に心配する必要はありません。

上記を考慮すると、インターフェースをthrowsセマンティクスで装飾することは、ほんのわずかな機能的利得にすぎません。なぜなら、インターフェースコントラクトを介した多くの呼び出し元は、なぜではなく、失敗した場合にのみ気にするからです。

私はそれが味と利便性の問題になると言うでしょう:あなたの主な焦点は、「例外」の後に呼び出し元と呼び出し先の両方であなたの状態を優雅に回復することです。 Cの背景から)、または例外が悪になる可能性のある環境で作業している場合(C ++)、古いものに頼ることができない、きれいできれいなOOPのために物を投げることが非常に重要であるとは思わないあなたがそれを不快にしている場合のパターン。特に、SoCの破壊につながる場合。

理論的な観点から、SoCコーシャーの例外処理方法は、ほとんどの場合、直接の呼び出し元が失敗したことを気にするだけであり、理由ではないという観察から直接導出できると思います。呼び出し先がスローし、非常に近い(2-3フレーム)誰かがアップキャストバージョンをキャッチし、実際の例外は常に(トレースのみであっても)特殊なエラーハンドラーにシンクされます。水平になりそうです。


1

エラーコードよりも例外を優先する

  • 両方が共存する必要があります。

  • 特定の動作が予想される場合は、エラーコードを返します。

  • 何らかの動作を予期していなかった場合に例外を返します。

  • エラーコードは通常、例外タイプが残っている場合に単一のメッセージに関連付けられますが、メッセージは異なる場合があります

  • エラーコードにない場合、例外にはスタックトレースがあります。壊れたシステムのデバッグにエラーコードを使用しません。

実装ではなくインターフェースへのコーディング

これはJAVAに固有の場合がありますが、インターフェイスを宣言するとき、そのインターフェイスの実装によってスローされる可能性のある例外を指定しませんが、それは意味がありません。

例外を確実にキャッチするとき、実装について仮定しているのでしょうか?

これは完全にあなた次第です。非常に特殊なタイプの例外をキャッチしてから、より一般的な例外をキャッチできますException。例外をスタックに伝播してから処理してみませんか?あるいは、例外処理が「プラグイン可能な」アスペクトになるアスペクトプログラミングを見ることができます。

実装が例外をスローする必要がない場合、または他の例外をスローする必要がある場合はどうなりますか?

なぜあなたにとってそれが問題なのか分かりません。はい、失敗しないか、例外をスローしない1つの実装があり、常に失敗して例外をスローする別の実装がある場合があります。その場合は、インターフェイスで例外を指定しないでください。問題は解決します。

例外の代わりに実装が結果オブジェクトを返した場合、それは何を変更しますか?このオブジェクトには、アクションの結果と、エラー/障害(ある場合)が含まれます。その後、そのオブジェクトに問い合わせることができます。


1

漏れやすい抽象化

インターフェイスでスローできる例外を指定する必要があるのはなぜですか?実装が例外をスローする必要がない場合、または他の例外をスローする必要がある場合はどうなりますか?インターフェイスレベルでは、実装がスローする例外を知る方法はありません。

私の経験では、エラーを受け取るコード(例外、エラーコード、その他のいずれかを介して)は通常、エラーの正確な原因を気にしません。エラー(エラーダイアログまたは何らかのログ); このレポートは、失敗したプロシージャを呼び出したコードに直交して実行されます。たとえば、このコードは特定のエラーの報告方法(メッセージ文字列のフォーマットなど)を知っている他のコードにエラーを渡し、場合によってはコンテキスト情報を添付します。

もちろん、場合によっては、特定のセマンティクスをエラーに添付し、発生したエラーに基づいて異なる対応をする必要があります。そのような場合は、インターフェース仕様に文書化する必要があります。ただし、インターフェイスは、特に意味のない他の例外をスローする権利を留保する場合があります。


1

例外を使用すると、エラーを報告および処理するためのより構造化された簡潔なコードを作成できることがわかります。

一方、例外は実装の詳細を明らかにすることに同意します。実装の詳細は、インターフェイスを呼び出すコードに隠すべきです。(Javaのようにメソッドシグネチャで宣言されていない限り)どのコードがどの例外をスローできるかを先験的に知ることはできないため、例外を使用することで、コードの異なる部分間に非常に複雑な暗黙的な依存関係を導入しています。依存関係を最小限に抑えるという原則に反します。

要約:

  • キャッチされていない例外は、エラーコードよりも目立ちやすく、無視するのが難しいため(例外はすぐに失敗するため)、例外を使用すると、よりクリーンなコードとテストおよびデバッグへのより積極的なアプローチが可能になります。
  • 一方、テスト中に発見されなかったキャッチされていない例外バグは、本番環境でクラッシュの形で現れる可能性があります。特定の状況では、この動作は受け入れられません。この場合、エラーコードを使用する方がより堅牢なアプローチだと思います。

1
同意しません。はい、キャッチされていない例外はアプリケーションをクラッシュさせる可能性がありますが、未チェックのエラーコードも同様にクラッシュする可能性があります。例外を適切に使用すると、キャッチされない例外は致命的な例外(OutOfMemoryなど)になります。これらの場合、すぐにクラッシュすることが最善です。
-sleske

エラーコードは、呼び出し元m1と呼び出し先m2の間のコントラクトの一部です。可能なエラーコードは、m2のインターフェイスでのみ定義されています。例外を使用すると(Javaを使用し、メソッドシグネチャでスローされたすべての例外を宣言しない限り)、呼び出し元m1とm2によって再帰的に呼び出されるすべてのメソッドとの間に暗黙のコントラクトがあります。したがって、返されたエラーコードをチェックしないのはもちろん間違いですが、それは常に可能です。一方、メソッドの実装方法がわからない限り、メソッドによってスローされるすべての例外を常にチェックできるとは限りません。
ジョルジオ

最初に、すべての例外をチェックできます-「ポケモン例外処理」を行うだけです(つまりcatch Exception、それらすべてをキャッチする必要があります-つまりThrowable、または、または同等のもの)。
-sleske

1
実際には、APIが適切に設計されている場合、クライアントが有意義に処理できるすべての例外を指定します。これらの例外は特にキャッチする必要があります。これらはエラーコードに相当します)。その他の例外は「内部エラー」を意味し、アプリケーションはシャットダウンするか、少なくとも対応するサブシステムをシャットダウンする必要があります。必要に応じてこれらをキャッチできます(上記を参照)が、通常はそれらをバブルアップさせる必要があります。その「バブリングアップ」は、例外を使用する主な利点です。必要に応じて、それらをさらにキャッチすることも、キャッチしないこともできます。
-sleske

-1

右偏りのどちらか。

無視することはできません。処理する必要があり、完全に透過的です。そして、正しい左利きのエラータイプを使用すると、Java例外と同じ情報がすべて伝えられます。

マイナス面?適切なエラー処理を備えたコードはうんざりしているように見えます(すべてのメカニズムに当てはまります)。


これは、以前の10の回答で作成および説明されたポイントに対して実質的なものを提供していないようです。なぜ、そのようなもので2歳の疑問をぶつけ
ブヨ

ここで誰も正しい偏りに言及していないことを除けば。hugomgはhaskellについて詳しく話しましたが、エラーが発生した理由の説明も、回復するための直接的な方法も、コールバックは制御フロー設計の最大の罪の1つではないため、たぶんたわごとエラーハンドラーかもしれません。そして、この質問がグーグルで出てきました。
キーナン14
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.