例外を使用して問題を早期に発見します。例えば:
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.
}
この場合、パラメータとしてのnull
はプログラムの周りに渡され、後でエラーが発生する可能性があり、Originまでさかのぼることは困難です。
そのように関数を変更する:
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
参照は単なる例です。これは、無効な引数から何かに至るまで、さまざまな種類の問題になる可能性があります。常に早期に発見することをお勧めします。
だから私の質問は単純です:これは良い習慣ですか?問題防止ツールとしての例外の使用は良いですか?これは例外の正当な適用ですか、それとも問題がありますか?
はい、「早期に失敗する」は非常に良い原則であり、これは単にそれを実装する1つの可能な方法です。そして、特定の値を返さなければならないメソッドでは、実際にはそれほど多くはありませんcan意図的に失敗するために行う-それは例外をスローするか、またはアサーションをトリガーします。例外は「例外的な」状態を示すことになっているため、プログラミングエラーの検出は例外的です。
はい、例外を投げることは良い考えです。早く投げ、頻繁に投げ、熱心に投げます。
「例外とアサーション」の議論があることは知っています。ある種の例外的な動作(特に、プログラミングエラーを反映していると考えられる動作)は、ビルドのデバッグ/テストではなく実行時に「コンパイル」できるアサーションによって処理されます。しかし、いくつかの追加の正当性チェックで消費されるパフォーマンスの量は、最新のハードウェアでは最小限であり、追加のコストは、正確で破損していない結果の価値よりはるかに重要です。実行時に(ほとんどの)チェックを削除したいアプリケーションのコードベースに実際に会ったことはありません。
数値を多用するコードのタイトなループ内で、追加のチェックや条件をたくさん使いたくないと言いたくなりますが、実際には、そこに多くの数値エラーが生成され、そこに捕捉されないと、外部に伝播します。すべての結果に影響します。だから、そこにもチェックを行う価値があります。実際、最良で最も効率的な数値アルゴリズムのいくつかは、エラー評価に基づいています。
追加のコードを非常に意識する最後の場所の1つは非常にレイテンシの影響を受けやすいコードで、追加の条件文がパイプラインのストールを引き起こす可能性があります。つまり、オペレーティングシステム、DBMS、その他のミドルウェアカーネル、および低レベルの通信/プロトコル処理の途中です。しかし、繰り返しになりますが、これらはエラーが最も発生する可能性が高い場所の一部であり、それらの(セキュリティ、正確性、およびデータの整合性)の影響が最も有害です。
私が見つけた1つの改善点は、基本レベルの例外のみをスローしないことです。 IllegalArgumentException
は適切ですが、基本的にどこからでも取得できます。ほとんどの言語では、カスタム例外を追加するのにそれほど時間はかかりません。個人処理モジュールの場合、次のように言います。
public class PersonArgumentException extends IllegalArgumentException {
public MyException(String message) {
super(message);
}
}
次に、誰かがPersonArgumentException
を見ると、それがどこから来たかがわかります。エンティティを不必要に増やしたくないので(Occam's Razor)、追加したいカスタム例外の数についてバランスを取る必要があります。多くの場合、「このモジュールが適切なデータを取得していない!」というシグナルを送るには、わずかなカスタム例外で十分です。または「このモジュールは、想定されていることを実行できません!」特定の方法で調整されますが、それほど正確ではないため、例外階層全体を再実装する必要があります。在庫の例外から始めてコードをスキャンし、「これらのNの場所は在庫の例外を発生させていますが、データを取得していないというより高いレベルの考えに要約されています。彼らが必要とするものです。これらの株式の例外を、実際に何が起こっているのかをより明確に伝えるより高いレベルの例外に置き換えましょう。」
アプリケーションをデバッグするときに、できるだけ早く失敗することは素晴らしいことです。レガシーC++プログラムの特定のセグメンテーション違反を覚えています:バグが検出された場所は、それが導入された場所とは関係がありませんでした(nullポインターがメモリ内のある場所から別の場所に移動され、最終的に問題が発生しました)。このような場合、スタックトレースは役に立ちません。
つまり、防御型プログラミングは、バグをすばやく検出して修正するための非常に効果的なアプローチです。一方、特にnull参照の場合は、やり過ぎることがあります。
たとえば、特定のケースでは、いずれかの参照がnullの場合、1人の年齢を取得しようとすると、次のステートメントでNullReferenceException
がスローされます。ここで実際に自分で確認する必要はありません:基盤となるシステムにエラーをキャッチさせ、例外をスローさせます。そのため、エラーが存在します。
より現実的な例として、assert
ステートメントを使用できます。
書き込みと読み取りが短い:
assert p1 : "p1 is null";
assert p2 : "p2 is null";
あなたのアプローチのために特別に設計されています。アサーションと例外の両方がある世界では、次のように区別できます。
したがって、アプリケーションの入力や状態に関する仮定をアサーションで公開すると、次の開発者はコードの目的をもう少し理解できます。
静的アナライザー(コンパイラーなど)の方が便利な場合もあります。
最後に、単一のスイッチを使用して、デプロイされたアプリケーションからアサーションを削除できます。しかし、一般的に言えば、それによって効率が向上することを期待しないでください。実行時のアサーションチェックはごくわずかです。
私の知る限り、さまざまなプログラマーがどちらか一方のソリューションを好んでいます。
最初の解決策は、より簡潔であり、特に、異なる条件で同じ条件を何度もチェックする必要がないため、通常は推奨されます。
私は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.
}
_
よりしっかりしている、なぜなら
registerPerson()
が呼び出されたときです。デバッグがはるかに簡単になります。無効な値がバグとして現れる前に、コード内をどこまで移動できるかは誰もが知っています。registerPerson()
は、他の関数がperson
引数を使用して最終的にどのように使用されるかについての仮定を行いません:null
の決定エラーが発生し、ローカルで実装されています。したがって、特にコードがかなり複雑な場合は、この2番目のアプローチを好む傾向があります。
一般に、はい、「早期に失敗」するのは良い考えです。ただし、具体的な例では、明示的なIllegalArgumentException
はNullReferenceException
を大幅に改善しません。これは、操作対象の両方のオブジェクトが関数の引数として既に提供されているためです。
しかし、少し異なる例を見てみましょう。
class PersonCalculator {
PersonCalculator(Person p) {
if (p == null) throw new ArgumentNullException("p");
_p = p;
}
void Calculate() {
// Do something to p
}
}
コンストラクターで引数のチェックがない場合、NullReferenceException
を呼び出すとCalculate
が返されます。
しかし、壊れたコードはCalculate
関数でも、Calculate
関数のコンシューマーでもありませんでした。壊れたコードは、ヌルPersonCalculator
を使用してPerson
を構築しようとするコードでした。そのため、ここで例外を発生させます。
その明示的な引数チェックを削除すると、NullReferenceException
が呼び出されたときにCalculate
が発生した理由を理解する必要があります。また、オブジェクトがnull
人で作成された理由を追跡することは、特に計算機を作成するコードが実際にCalculate
関数を呼び出すコードに近くない場合、注意が必要です。
あなたが与える例ではありません。
あなたが言うように、その後すぐに例外を受け取るつもりなら、明示的に投げることはあなたに多くをもたらしません。私は同意しませんが、多くの人は、明確な例外に適切なメッセージを付けた方がよいと主張します。リリース前のシナリオでは、スタックトレースで十分です。リリース後のシナリオでは、呼び出しサイトが関数内よりも優れたメッセージを提供できることがよくあります。
2番目の形式は、関数に提供する情報が多すぎます。その関数は、他の関数がnull入力をスローすることを必ずしも認識している必要はありません。 null入力nowをスローする場合でも、nullチェックがコード全体に分散されているため、そうでなくてもリファクタリングが非常に面倒になります。
しかし、一般的に、何かがおかしいと判断したら(DRYを尊重しながら)早く投げる必要があります。これらはそれの良い例ではないかもしれません。