エラーを早期に「キャッチ」するツールとして例外を使用しても大丈夫ですか?


29

例外を使用して問題を早期に発見します。例えば:

public int getAverageAge(Person p1, Person p2){
    if(p1 == null || p2 == null)
        throw new IllegalArgumentException("One or more of input persons is null").
    return (p1.getAge() + p2.getAge()) / 2;
}

私のプログラムはnullこの関数を決して渡すべきではありません。私はそれをするつもりはありません。しかし、私たち全員が知っているように、プログラミングでは意図しないことが起こります。

この問題が発生した場合に例外をスローすると、プログラムの他の場所でさらに問題が発生する前に、それを見つけて修正することができます。例外はプログラムを停止し、「ここで悪いことが起こったので修正してください」と言っています。これnullがプログラム内を移動する代わりに、他の場所で問題を引き起こします。

さて、あなたは正しいです。この場合、これはただちにすぐにnull発生するNullPointerExceptionため、最良の例ではないかもしれません。

しかし、たとえば次のような方法を検討してください。

public void registerPerson(Person person){
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

この場合、nullas asパラメータはプログラムの周りに渡され、はるか後にエラーが発生する可能性があり、そのエラーを元に戻すのは困難です。

関数を次のように変更します。

public void registerPerson(Person person){
    if(person == null) throw new IllegalArgumentException("Input person is null.");
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

他の場所で奇妙なエラーが発生する前に問題を見つけることができます。

また、nullパラメーターとしての参照は単なる例です。無効な引数から他のものまで、さまざまな問題が発生する可能性があります。それらを早期に発見することは常に良いことです。


だから私の質問は単純です:これは良い習慣ですか?問題防止ツールとしての例外の使用は適切ですか?これは例外の正当なアプリケーションですか、それとも問題がありますか?


2
downvoterはこの質問の何が悪いのか説明できますか?
ジョルジオ

2
「早期クラッシュ、頻繁にクラッシュ」
ブライアンチェン

16
それは文字通り例外がそこにあるものです。
raptortech97 14

コーディング規約により、こうしたエラーの多くを静的に検出できることに注意してください。通常、これは実行時に例外をスローするよりも優れています。コーディング契約のサポートと有効性は、言語によって異なります。
ブライアン14

3
IllegalArgumentExceptionをスローしているため、「入力者」と言うのは冗長です。
ケビンクライン14

回答:


29

はい、「早期に失敗する」ことは非常に良い原則であり、これは単にそれを実装する1つの可能な方法です。そして、特定の値を返さなければならないメソッドでは、意図的に失敗するためにできることはほとんどありません。例外をスローするか、アサーションをトリガーすることです。例外は「例外的な」条件を通知するためのものであり、プログラミングエラーの検出は確かに例外的です。


エラーや状態から回復する方法がない場合、早期に失敗する方法があります。たとえば、ファイルをコピーする必要があり、ソースまたは宛先のパスが空の場合、すぐに例外をスローする必要があります。
マイケルショップシン14

また、「高速不合格」として知られている
keuleJ

8

はい、例外をスローすることをお勧めします。早く投げ、頻繁に投げ、熱心に投げます。

私は、「例外とアサーション」の議論があり、ある種の例外的な動作(特に、プログラミングエラーを反映すると考えられるもの)が、ビルドをデバッグ/テストするのではなく実行時に「コンパイル」できるアサーションによって処理されることを知っています。しかし、最新のハードウェアでは、いくつかの追加の正確性チェックで消費されるパフォーマンスの量は最小限であり、追加のコストは、正しい、破損していない結果を得る価値によってはるかに上回ります。実行時に(ほとんどの)チェックを削除したいアプリケーションコードベースに実際に会ったことはありません。

私は、数値的に集中したコードのタイトなループ内で余分なチェックと条件をあまり望んでいないと言いたくなります...しかし、実際にはそこに多くの数値エラーが生成され、そこでキャッチされない場合、外側に伝播しますすべての結果に影響します。そこでも、チェックする価値はあります。実際、最良で最も効率的な数値アルゴリズムのいくつかは、エラー評価に基づいています。

余分なコードを非常に意識する最後の場所は、非常にレイテンシに敏感なコードです。ここでは、余分な条件がパイプラインのストールを引き起こす可能性があります。そのため、オペレーティングシステム、DBMS、その他のミドルウェアカーネル、および低レベルの通信/プロトコル処理の途中で。しかし、繰り返しますが、これらはエラーが観察される可能性が最も高い場所の一部であり、それらの(セキュリティ、正確性、およびデータの整合性)の影響は最も有害です。

私が見つけた改善の1つは、基本レベルの例外のみをスローしないことです。IllegalArgumentException良いですが、それは本質的にどこからでも来ることができます。ほとんどの言語では、カスタム例外を追加するのにそれほど時間はかかりません。個人処理モジュールの場合:

public class PersonArgumentException extends IllegalArgumentException {
    public MyException(String message) {
        super(message);
    }
}

その後、誰かが見たとき PersonArgumentException、それがどこから来たのかは明らかです。エンティティを不必要に乗算する必要がないため(OccamのRazor)、追加するカスタム例外の数についてバランスをとる行為があります。多くの場合、「このモジュールは適切なデータを取得していません!」というシグナルを出すには、わずかなカスタム例外で十分です。または「このモジュールは、本来行うべきことを実行できません!」特定の方法で調整されていますが、あまり正確ではないため、例外階層全体を再実装する必要があります。在庫例外から始めてコードをスキャンし、「これらのN個の場所は在庫例外を発生させているが、データを取得していないという上位概念に要約されている」彼らが必要です;しましょう


I've never actually met an application codebase for which I'd want (most) checks removed at runtime. 次に、パフォーマンスに重要なコードを実行していません。私は現在、アサーションをコンパイルして毎秒37Mの操作を行い、アサーションなしで42Mの操作を行うものに取り組んでいます。アサーションは外部入力を検証するものではなく、コードが正しいことを確認するためにあります。荷物が壊れていないことに満足したら、顧客は13%の増加を喜んで受けます。
Blrfl 14

カスタム例外を使用してコードベースをペッパーリングすることは避けるべきです。通常、共通の例外を拡張し、メンバーや機能を追加しない場合は、実際には必要ありません。あなたの例でも、PersonArgumentExceptionほど明確ではありませんIllegalArgumentException。後者は、不正な引数が渡されたときにスローされることが広く知られています。a Personが呼び出しに対して無効な状態にある場合、実際には前者がスローされることを期待しています(InvalidOperationExceptionC#と同様)。
セラリアドボール14

気をつけなければ、関数のどこかから同じ例外をトリガーできるのは事実ですが、それは一般的な例外の性質ではなく、コードの欠陥です。そして、そこがデバッグとスタックトレースの出番です。例外に「ラベル付け」しようとしている場合は、メッセージコンストラクターの使用など、最も一般的な例外が提供する既存の機能を使用します(まさにそのためです)。一部の例外は、問題のある引数の名前を取得するための追加のパラメーターをコンストラクターに提供しますが、整形式のメッセージで同じ機能を提供できます。
セラリアドボール14

基本的に同じ例外のプライベートラベルに共通の例外をブランド変更するだけでは弱い例であり、努力する価値はほとんどないことを認めます。実際には、より高いセマンティックレベルで、モジュールの意図に密接に結び付けられたカスタム例外を発行しようとします。
ジョナサンユニス

数値/ HPCおよびOS /ミドルウェアレルムでパフォーマンスに敏感な作業を行いました。13%のパフォーマンスバンプは決して小さくありません。軍事司令官が公称原子炉出力の105%を要求するのと同じ方法で、それを得るためにいくつかのチェックをオフにするかもしれません。しかし、チェック、バックアップ、およびその他の保護をオフにする理由として頻繁に与えられる「この方法でより高速に実行される」を見てきました。基本的に、復元力と安全性(多くの場合、データの整合性を含む)を犠牲にしてパフォーマンスを向上させます。それが価値があるかどうかは判断の呼び出しです。
ジョナサンユニス14

3

アプリケーションをデバッグするとき、できるだけ早く失敗するのは素晴らしいことです。レガシーC ++プログラムの特定のセグメンテーションフォールトを覚えています:バグが検出された場所は、それが導入された場所とは何の関係もありませんでした(最終的に問題を引き起こす前に、nullポインターがメモリ内のある場所から別の場所に移動されました) )。このような場合、スタックトレースは役に立ちません。

防御プログラミングは、バグを迅速に検出して修正するための非常に効果的なアプローチです。一方、特にnull参照の場合は、やり過ぎになる可能性があります。

たとえば、特定の場合:参照のいずれかがnullのNullReferenceException場合、1人の年齢を取得しようとしたときに、次のステートメントで参照がスローされます。ここで自分で物事を確認する必要はありません。基礎となるシステムにこれらのエラーをキャッチさせ、例外をスローさせてください。

より現実的な例として、次のassertステートメントを使用できます。

  1. 読み書きが短くなります:

        assert p1 : "p1 is null";
        assert p2 : "p2 is null";
    
  2. あなたのアプローチのために特別に設計されています。アサーションと例外の両方がある世界では、次のように区別できます。

    • アサーションプログラミングエラーです( "/ *これは絶対に発生しないはずです* /")。
    • 例外は、コーナーケースの場合です(例外的ではあるが可能性のある状況)

したがって、アプリケーションの入力および/または状態に関する仮定をアサーションで公開すると、次の開発者はコードの目的をもう少し理解することができます。

静的アナライザー(例:コンパイラー)も幸せかもしれません。

最後に、単一のスイッチを使用して、デプロイされたアプリケーションからアサーションを削除できます。しかし、一般的に言えば、それによる効率の向上は期待しないでください。実行時のアサーションチェックはごくわずかです。


1

私の知る限り、さまざまなプログラマーがどちらかのソリューションを好みます。

最初の解決策は、より簡潔であるため、通常は好まれます。特に、異なる機能で同じ条件を何度もチェックする必要がないためです。

私は2番目の解決策を見つけます、例えば

public void registerPerson(Person person){
    if(person == null) throw new IllegalArgumentException("Input person is null.");
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

より堅実

  1. エラーをできるだけ早くキャッ​​チします。つまりregisterPerson()、コールスタックのどこかでNULLポインター例外がスローされたときではなく、呼び出されたときです。デバッグがはるかに簡単になります:無効な値がバグとして現れる前にコードをどの程度まで移動できるかは誰でも知っています。
  2. これにより、関数間の結合が減少します。引数をregisterPerson()使用する他の関数とそのperson使用方法についての仮定は行われませんnull。エラーである決定が行われ、ローカルで実装されます。

そのため、特にコードがかなり複雑な場合は、この2番目のアプローチを好む傾向があります。


1
理由( "Input person is null。")を与えると、ライブラリ内の不明瞭なメソッドが失敗した場合とは異なり、問題を理解するのにも役立ちます。
フローリアンF 14

1

一般に、はい、「早期に失敗する」のは良い考えです。ただし、特定の例では、明示的な操作でIllegalArgumentExceptionNullReferenceException- に対する大幅な改善は提供されません。これは、操作対象の両方のオブジェクトが関数の引数として既に配信されているためです。

しかし、少し異なる例を見てみましょう。

class PersonCalculator {
    PersonCalculator(Person p) {
        if (p == null) throw new ArgumentNullException("p");
        _p = p;
    }

    void Calculate() {
        // Do something to p
    }
}

コンストラクターで引数のチェックがなかったNullReferenceException場合、を呼び出すとが取得されますCalculate

しかし、壊れたコードはCalculate関数でも関数のコンシューマでもありませんでしたCalculate。壊れたコードは、PersonCalculatornullを使用してを作成しようとするコードでしたPerson。そこで、例外を発生させます。

明示的な引数チェックを削除するNullReferenceException場合、Calculateが呼び出されたときにa が発生した理由を把握する必要があります。そしてnull、特に電卓を構築するコードが実際にCalculate関数を呼び出すコードに近くない場合、人でオブジェクトが構築された理由を追跡するのは難しいかもしれません。


0

あなたが与える例ではありません。

あなたが言うように、あなたがその後すぐに例外を取得しようとしているとき、明示的に投げることはあまり得られません。私は同意しませんが、多くの人は、明確な例外を良いメッセージと共に持つ方が良いと主張します。事前にリリースされたシナリオでは、スタックトレースで十分です。リリース後のシナリオでは、呼び出しサイトは多くの場合、関数内よりも優れたメッセージを提供できます。

2番目の形式は、関数に提供する情報が多すぎます。その関数は、他の関数がnull入力でスローすることを必ずしも知っている必要はありません。彼らはヌル入力にスローを行う場合であっても、今その停止が場合されなければならないのnullチェックがコード全体に広がっているので、それはリファクタリングに非常に面倒になります。

しかし、一般的に、何かが間違っていると判断したら(DRYを尊重しながら)早く投げるべきです。しかし、これらはおそらくその良い例ではありません。


-3

あなたの例の関数では、チェックを行わずに、ただ実行することを許可することをお勧めしますNullReferenceException

1つは、とにかくnullを渡すことには意味がないため、のスローに基づいて問題をすぐに把握しますNullReferenceException

2つ目は、明らかに間違った入力の種類に基づいてすべての関数がわずかに異なる例外をスローしている場合、すぐに18種類の例外をスローする可能性のある関数があり、すぐにそれも言っていることがわかります対処するための多くの作業、とにかくすべての例外を抑制するだけです。

関数の設計時エラーの状況を解決するために実際にできることは何もないので、変更せずに障害を発生させてください。


私はこの原則に基づいたコードベースで数年間取り組んできました。ポインターは必要に応じて単に逆参照され、NULLに対してチェックされることはほとんどなく、明示的に処理されません。このコードのデバッグは、例外(またはコアダンプ)が論理エラーのあるポイントよりもずっと後に発生するため、常に非常に時間がかかり、面倒です。特定のバグを探すために文字通り何週間も無駄にしました。これらの理由から、少なくともnullポインター例外がどこから来たのかがすぐにはわからない複雑なコードの場合は、できる限り早く失敗するスタイルを好みます。
ジョルジオ14

「1つは、とにかくnullを渡すのは意味がないので、NullReferenceExceptionのスローに基づいてすぐに問題を見つけます。」:必ずしも、Progの2番目の例が示すように。
ジョルジオ14

「2つは、明らかに間違った入力の種類に基づいてすべての関数がわずかに異なる例外をスローする場合、18種類の例外をスローする可能性のある関数がすぐに得られるということです...」:この場合、すべての関数で同じ例外(NullPointerExceptionも)を使用できます。重要なのは、各関数から異なる例外をスローするのではなく、早期にスローすることです。
ジョルジオ14

それがRuntimeExceptionの目的です。「起こるべきではない」が、コードの動作を妨げる条件は、1つスローする必要があります。メソッドシグネチャで宣言する必要はありません。ただし、ある時点でそれらをキャッチし、要求されたアクションが失敗した機能を報告する必要があります。
フロリアンF 14

1
質問は言語を指定しないので、例えばに頼ることRuntimeExceptionは必ずしも有効な仮定ではありません。
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.