web-dev-qa-db-ja.com

例外は例外的な場合にのみ使用するべきだと言われました。私のケースが例外的であるかどうかはどうすればわかりますか?

ここでの私の具体的なケースは、ユーザーが文字列をアプリケーションに渡すことができ、アプリケーションがそれを解析して構造化オブジェクトに割り当てることです。時々、ユーザーは何か無効なものを入力するかもしれません。たとえば、彼らの入力は人を表すかもしれませんが、彼らの年齢は「アップル」であると言うかもしれません。その場合の正しい動作は、トランザクションをロールバックし、エラーが発生したことをユーザーに通知することです。ユーザーは再試行する必要があります。最初のエラーだけでなく、入力で見つけることができるすべてのエラーについて報告する必要がある場合があります。

この場合、例外をスローする必要があると主張しました。 「例外は例外的である必要があります:ユーザーが無効なデータを入力する可能性があるため、これは例外的なケースではありません」と彼は反対しました。Wordの定義により、彼はその点について議論する方法を本当に知りませんでした。正しいようです。

しかし、これが例外が最初に発明された理由だと私は理解しています。以前はあなたでしたhad結果を調べてエラーが発生したかどうかを確認しました。チェックに失敗すると、気付かないうちに悪いことが起こります。

例外なく、スタックのすべてのレベルで、呼び出すメソッドの結果を確認する必要があり、プログラマーがこれらのレベルの1つをチェックインし忘れた場合、コードが誤って処理を続行し、たとえば無効なデータを保存する可能性があります。その方法でエラーが発生しやすくなります。

とにかく、ここで私が言ったことを自由に修正してください。私の主な質問は、例外は例外的である必要があると誰かが言った場合、私のケースが例外的であるかどうかをどうやって知るのですか?

102
Daniel Kaplan

例外は、コードの混乱を減らしてエラー処理を簡単にするために考案されました。コードの乱雑さが減り、エラー処理が容易になる場合に使用してください。この「例外的な状況のみの例外」ビジネスは、例外処理が許容できないパフォーマンスヒットと見なされた時期から生じています。大多数のコードではもはやそうではありませんが、人々はまだその背後にある理由を思い出さずにルールを口にしています。

特に、Javaは、これまで考えられた中で最も例外を好む言語である可能性が高いため、例外の使用について気にしないでくださいコードを単純化する場合実際、Java独自のIntegerクラスNumberFormatExceptionをスローすることなく、文字列が有効な整数かどうかをチェックする手段はありません。

また、UI検証にjustを信頼することはできませんが、スピナーを使用して短い数値を入力するなど、UIが適切に設計されている場合は注意してください値の場合、非数値の値をバックエンドに入れると、例外的な状態になります。

88
Karl Bielefeldt

例外がスローされるのはいつですか?コードに関しては、次の説明が非常に役立つと思います。

メンバーは、名前で示されているように、メンバーが実行することになっているタスクを完了できなかった場合。 (Jeffry Richter、C#経由のCLR)

なぜ役立つのですか?それは、何かが例外として扱われるべきかどうかのコンテキストに依存することを示唆しています。メソッド呼び出しのレベルでは、コンテキストは(a)名前、(b)メソッドのシグネチャ、および(b)メソッドを使用する、または使用することが予期されるクライアントコードによって与えられます。

質問に答えるには、ユーザー入力が処理されるコードを確認する必要があります。次のようになります。

public void Save(PersonData personData) { … }

メソッド名は、いくつかの検証が行われたことを示唆していますか?いいえ。この場合、無効なPersonDataは例外をスローします。

クラスに次のような別のメソッドがあるとします。

public ValidationResult Validate(PersonData personData) { … }

メソッド名は、いくつかの検証が行われたことを示唆していますか?はい。この場合、無効なPersonDataは例外をスローしません。

まとめると、どちらの方法でも、クライアントコードは次のようになるはずです。

ValidationResult validationResult = personRegister.Validate(personData);
if (validationResult.IsValid())
{
    personRegister.Save(personData)
}
else
{
    // Throw an exception? To answer this look at the context!
    // That is: (a) Method name, (b) signature and
    // (c) where this method is (expected) to be used.
}

メソッドが例外をスローする必要があるかどうかが明確でない場合は、メソッド名またはシグネチャの選択が不適切である可能性があります。たぶん、クラスのデザインは明確ではありません。例外をスローする必要があるかどうかの質問に対する明確な回答を得るために、コードデザインを変更する必要がある場合があります。

72
Theo Lenndorff

例外は例外的である必要があります:ユーザーが無効なデータを入力する可能性があるため、これは例外的なケースではありません

その議論について:

  • ファイルが存在しない可能性があるため、例外ではありません。
  • サーバーへの接続が失われる可能性があるため、例外ではありません
  • 例外ではないので、設定ファイルが文字化けする可能性があります
  • リクエストが失敗することが予想されるため、例外ではありません

あなたがキャッチする例外は、あなたがそれをキャッチすることにしたので、あなたは期待しなければなりません。したがって、このロジックでは、実際にキャッチする予定の例外をスローしないでください。

したがって、「例外は例外的でなければならない」とは、ひどい経験則だと思います。

何をすべきかは言語によって異なります。例外をスローするタイミングについては、言語によって異なる規則があります。たとえば、Pythonはすべてに対して例外をスローしますが、Pythonの場合も同様です。一方、C++は比較的少ない例外をスローしますが、私はそれに倣っています。 C++またはJava Pythonのように扱い、すべてに対して例外をスローすることができますが、言語がそれ自体が使用されることを期待する方法と対立します。

私はPythonのアプローチを好みますが、他の言語をそれにかけるのは悪い考えだと思います。

31
Winston Ewert

例外について考えるとき、私はいつもデータベースサーバーやWeb APIにアクセスするようなことを考えています。サーバー/ Web APIが機能することを期待していますが、例外的なケースでは機能しない可能性があります(サーバーがダウンしています)。通常、Webリクエストは高速ですが、例外的な状況(高負荷)ではタイムアウトになることがあります。これはあなたの手に負えないものです。

ユーザーが送信したものをチェックして、好きなように処理できるため、ユーザーの入力データを制御できます。あなたの場合、保存する前にユーザー入力を検証します。また、ユーザーが無効なデータを提供することは予期されているべきであり、アプリは入力を検証してユーザーフレンドリーなエラーメッセージを提供することでそれを説明することに同意する傾向があります。

とは言っても、ほとんどのドメインモデルセッターでは例外を使用しています。無効なデータが入る可能性はまったくありません。しかし、これは最後の防衛線であり、豊富な検証ルールを使用して入力フォームを作成する傾向があります、そのドメインモデルの例外をトリガーする可能性はほとんどありません。したがって、セッターが1つのことを期待し、別のものを取得する場合、それは例外的な状況であり、通常の状況では発生してはならないことでした。

編集(考慮すべき他の何か):

ユーザーが提供したデータをデータベースに送信するとき、テーブルに何を入力すべきか、何を入力してはならないかを事前に知っています。これは、データをいくつかの予期される形式に対して検証できることを意味します。これはあなたが制御できるものです。制御できないのは、クエリの途中でサーバーが失敗することです。したがって、クエリに問題がなく、データがフィルター処理/検証されていることがわかっている場合、クエリを実行しても失敗しますが、これは例外的な状況です。

Webリクエストと同様に、リクエストを送信しようとする前に、リクエストがタイムアウトするか、接続に失敗するかを知ることができません。したがって、リクエストを送信するときに数ミリ秒後にサーバーが機能するかどうかをサーバーに尋ねることができないため、これはtry/catchアプローチも保証します。

30
Ivan Pintar

参照

から The Pragmatic Programmer:

例外がプログラムの通常のフローの一部として使用されることはめったにないはずです。例外は予期しないイベントのために予約する必要があります。キャッチされなかった例外によってプログラムが終了し、「すべての例外ハンドラーを削除しても、このコードは引き続き実行されますか?」答えが「いいえ」の場合、例外が例外的でない状況で使用されている可能性があります。

彼らは読み取りのためにファイルを開く例を調べ続けます、そしてファイルは存在しません-それは例外を発生させるべきですか?

ファイルが存在する必要がある場合、例外が保証されます。 [...]一方、ファイルが存在する必要があるかどうかわからない場合は、ファイルが見つからなくても例外的ではなく、エラーリターンが適切です。

後で、彼らはなぜ彼らがこのアプローチを選んだかについて議論します:

[A] n例外は、ローカルではない制御の即時転送を表します。これは、一種のカスケードgotoです。通常の処理の一部として例外を使用するプログラムは、古典的なスパゲッティコードのすべての可読性と保守性の問題を抱えています。これらのプログラムはカプセル化を解除します。ルーチンとその呼び出し元は、例外処理を介してより密接に結合されています。

あなたの状況について

あなたの質問は、「検証エラーが例外を発生させるべきか?」に要約されます。答えは、検証が行われている場所に依存するということです。

問題のメソッドが、入力データが既に検証されていると想定されるコードのセクション内にある場合、無効な入力データは例外を発生させるはずです。このメソッドがユーザーが入力した正確な入力を受け取るようにコードが設計されている場合、無効なデータが予期され、例外が発生してはなりません。

16
Mike Partridge

ここには多くの哲学的実証がありますが、一般的に言えば、例外的な条件は単にそれらの条件処理できない、または処理したくない(クリーンアップ、エラー報告など以外)ユーザーの介入なし。つまり、回復不可能な状態です。

何らかの方法でそのファイルを処理するつもりで、プログラムにファイルパスを渡し、そのパスで指定されたファイルが存在しない場合、それは例外的な状態です。ユーザーに対してそれを報告し、ユーザーが別のファイルパスを指定できるようにすることを除いて、コードに対してそれを行うことはできません。

11
Robert Harvey

考慮すべき2つの懸念事項があります。

  1. 単一の懸念事項について議論します-これをAssignerと呼びましょう。この懸念事項は、構造化オブジェクトへの入力の割り当てであるため、その入力が有効であるという制約を表します

  2. a well-implementedユーザーインターフェイスには追加の懸念事項があります。ユーザー入力の検証とエラーに関する建設的なフィードバック(この部分をValidatorと呼びましょう)

違反した制約を表現したので、Assignerコンポーネントの観点からは、例外のスローは完全に合理的です。

ユーザーエクスペリエンスの観点から見ると、ユーザーはそもそもこのAssignerに直接話しかけるべきではありません。彼らはそれに話しかけるべきですvia the Validator

さて、Validatorでは、無効なユーザー入力はnotが例外的なケースです。これは、実際にはより興味があるケースです。したがって、ここでは例外は適切ではなく、これはまた、最初にエラーを回避するのではなく、allエラーを識別したい場合もあります。

これら懸念事項がどのように実装されているかについては言及していません。あなたはAssignerについて話しているようで、同僚はValidator+Assigner。そこに気づいたらある 2つの別個の(または分離可能な)懸念事項であり、少なくともそれを慎重に議論することができます。


Renanのコメントに対処するために、私はただ仮定です。2つの個別の懸念を特定したら、各コンテキストでどのケースを例外と見なすべきかは明らかです。

実際、それが例外的であると見なされるべきかどうかがis n't明らかである場合、おそらくソリューションの独立した懸念事項の特定が完了していないと思います。

それが直接答えになると思います

...私のケースが例外的であるかどうかはどうすればわかりますか?

明白になるまで簡略化し続けます。よく理解している単純な概念の山がある場合は、それらをコード、クラス、ライブラリなどに再構成することを明確に推論できます。

7
Useless

他の人はよく答えましたが、それでもここに私の短い答えがあります。例外は、環境内の何かに問題があり、制御できず、コードをまったく進めない状況です。この場合、何が問題だったか、それ以上進めない理由、および解決策をユーザーに通知する必要があります。

4
Manoj R

私は、例外的な場合にのみ例外をスローする必要があるというアドバイスの大ファンではありませんでした。これは、何も言わないため(たとえば、食べられる食べ物だけを食べるべきだと言うようなものです)、また非常に主観的であり、例外的なケースを構成するものとそうでないものを明確にしないことがよくあります。

ただし、このアドバイスには十分な理由があります。例外のスローとキャッチが遅いため、Visual Studioのデバッガーでコードを実行し、例外がスローされるたびに通知するように設定している場合、何十ものスパムにさらされる可能性があります。問題が発生するずっと前に、何百ものメッセージが送信されます。

したがって、一般的なルールとして、

  • あなたのコードにはバグがありません、そして
  • 依存するサービスはすべて利用可能であり、
  • ユーザーが使用することを意図した方法でプログラムを使用している(ユーザーが提供する入力の一部が無効であっても)

その後、コードは例外をスローするべきではありません。無効なデータをトラップするには、UIレベルのバリデーターまたはプレゼンテーションレイヤーのInt32.TryParse()などのコードを使用できます。

それ以外の場合は、原則として例外は、メソッドの名前が示すとおりにメソッドが実行できないことを意味します。一般に、失敗を示すためにリターンコードを使用することはお勧めしません(メソッド名がそうであることを明確に示さない限り、たとえば、2つの理由からTryParse())です。まず、エラーコードに対するデフォルトの応答は、エラー状態を無視し、関係なく続行することです。次に、戻りコードを使用するいくつかのメソッドと例外を使用する他のメソッドで終わり、どれがどれであるかを忘れてしまう可能性があります。同じインターフェイスの2つの異なる交換可能な実装がここで異なるアプローチを取るコードベースを見たことさえあります。

3
jammycakes

最初のエラーだけでなく、入力で見つけることができるすべてのエラーについて報告する必要がある場合があります。

これが、ここで例外をスローできない理由です。例外が発生するとすぐに検証プロセスが中断されます。したがって、これを行うには多くの回避策があります。

悪い例:

例外を使用したDogクラスの検証メソッド:

void validate(Set<DogValidationException> previousExceptions) {
    if (!DOG_NAME_PATTERN.matcher(this.name).matches()) {
        DogValidationException disallowedName = new DogValidationException(Problem.DISALLOWED_DOG_NAME);
        if (!previousExceptions.contains(disallowedName)){
            throw disallowedName;
        }
    }
    if (this.legs < 4) {
        DogValidationException invalidDog = new DogValidationException(Problem.LITERALLY_INVALID_DOG);
        if (!previousExceptions.contains(invalidDog)){
            throw invalidDog;
        }
    }
    // etc.
}

それを呼び出す方法:

Set<DogValidationException> exceptions = new HashSet<DogValidationException>();
boolean retry;
do {
    retry = false;
    try {
        dog.validate(exceptions);
    } catch (DogValidationException e) {
        exceptions.add(e);
        retry = true;
    }
} while (retry);

if(exceptions.isEmpty()) {
    dogDAO.beginTransaction();
    dogDAO.save(dog);
    dogDAO.commitAndCloseTransaction();
} else {
    // notify user to fix the problems
}

ここでの問題は、すべてのエラーを取得するために、検証プロセスですでに見つかった例外をスキップする必要があることです。上記は機能しますが、これは明らかに例外の誤用です。要求された種類の検証は、データベースに触れる前に前に行う必要があります。したがって、何もロールバックする必要はありません。また、検証の結果は検証エラーになる可能性があります(ただし、できればゼロ)。

より良いアプローチは:

メソッド呼び出し:

Set<Problem> validationResults = dog.validate();
if(validationResults.isEmpty()) {
    dogDAO.beginTransaction();
    dogDAO.save(dog);
    dogDAO.commitAndCloseTransaction();
} else {
    // notify user to fix the problems
}

検証方法:

Set<Problem> validate() {
    Set<Problem> result = new HashSet<Problem>();
    if(!DOG_NAME_PATTERN.matcher(this.name).matches()) {
        result.add(Problem.DISALLOWED_DOG_NAME);
    }
    if(this.legs < 4) {
        result.add(Problem.LITERALLY_INVALID_DOG);
    }
    // etc.
    return result;
}

なぜ?理由は山ほどあり、他の回答でも多くの理由が指摘されています。簡単に言うと、はるかに簡単です他の人が読んで理解することです。次に、ユーザースタックトレースを表示して、彼がdogを誤って設定したことを説明しますか?

If、2番目の例のコミット中にまだエラーが発生します、バリデーターが問題なくdogを検証した場合でも、-その後例外をスローするのは正しいことです。同様:データベース接続がない、データベースエントリが他の人によって修正されている、など。

2
Matthias Ronge

例外は、その可能性が高い条件を表す必要があります すぐ 呼び出し側のメソッドが処理できる場合でも、呼び出し側のコードは処理する準備ができていません。たとえば、ファイルから一部のデータを読み取り、有効なファイルが有効なレコードで終わると正当に想定し、部分的なレコードから情報を抽出する必要がないコードを考えてみます。

データ読み取りルーチンが例外を使用せず、読み取りが成功したかどうかを報告するだけの場合、呼び出しコードは次のようになります。

temp = dataSource.readInteger();
if (temp == null) return null;
field1 = (int)temp;
temp = dataSource.readInteger();
if (temp == null) return null;
field2 = (int)temp;
temp = dataSource.readString();
if (temp == null) return null;
field3 = temp;

有用な作業ごとに3行のコードを費やすなど。対照的に、readIntegerがファイルの終わりに達したときに例外をスローし、呼び出し元が単に例外を渡すことができる場合、コードは次のようになります。

field1 = dataSource.readInteger();
field2 = dataSource.readInteger();
field3 = dataSource.readString();

物事が正常に機能する場合に非常に重点を置いた、はるかにシンプルでクリーンな外観。即時の呼び出し元wouldが条件を処理することを期待している場合、例外をスローするメソッドよりもエラーコードを返すメソッドの方が役立つことが多いことに注意してください。たとえば、ファイル内のすべての整数を合計するには:

do
{
  temp = dataSource.tryReadInteger();
  if (temp == null) break;
  total += (int)temp;
} while(true);

versus

try
{
  do
  {
    total += (int)dataSource.readInteger();
  }
  while(true);
}
catch endOfDataSourceException ex
{ // Don't do anything, since this is an expected condition (eventually)
}

整数を要求するコードは、それらの呼び出しの1つが失敗することを期待しています。コードがそれが発生するまで実行される無限ループを使用することは、戻り値を介して失敗を示すメソッドを使用するよりもはるかに洗練されていません。

多くの場合、クラスはクライアントが予期する条件または予期しない条件を認識しないため、一部の呼び出し側が期待する方法と他の呼び出し側が期待しない方法で失敗する可能性のある2つのバージョンのメソッドを提供すると便利です。そうすることで、そのようなメソッドを両方のタイプの呼び出し元できれいに使用できるようになります。呼び出し元が予期していない状況が発生した場合、「try」メソッドでも例外をスローする必要があることにも注意してください。たとえば、tryReadIntegerは、クリーンなファイルの終わり条件に遭遇しても例外をスローするべきではありません(呼び出し元がそれを予期していなかった場合、呼び出し元はreadIntegerを使用したはずです)。一方、データが読み取れなかった場合は、おそらく例外をスローするはずです。それを含むメモリースティックが抜かれました。このようなイベントは常に可能性として認識される必要がありますが、即時呼び出しコードが応答に役立つ何かを実行する準備ができているとは考えられません。ファイルの終わりの状態と同じように報告されることはありません。

2
supercat

ソフトウェアを書く上で最も重要なことは、ソフトウェアを読みやすくすることです。他のすべての考慮事項は、効率化や修正など、二次的なものです。読み取り可能な場合は、メンテナンスで残りの部分を処理できます。読み取り可能でない場合は、単に破棄することをお勧めします。したがって、読みやすさを向上させる場合は、例外をスローする必要があります。

アルゴリズムを書いているときは、それを読む将来の人について考えてください。潜在的な問題が発生する可能性のある場所に来たら、読者がその問題の処理方法を確認したいかどうかnowか、または読者はアルゴリズムを使い続けることを好みますか?

チョコレートケーキのレシピを考えたいです。卵を追加するように指示されたら、選択肢があります。卵があると想定してレシピを取得するか、卵がない場合に卵を取得する方法の説明を開始することができます。それはあなたがケーキを焼くのを助けるために野生のニワトリを狩猟するためのテクニックで本全体を埋めることができます。それは良いことですが、ほとんどの人はそのレシピを読みたくないでしょう。ほとんどの人は、卵が手元にあると思い込んで、レシピを続けます。これは、作者がレシピを書くときに必要な判断の呼びかけです。

読者の心を読む必要があるので、何が良い例外を作り、どんな問題がすぐに処理されるべきかについての保証されたルールはありません。あなたが今までにできる最善のことは経験則であり、「例外は例外的な状況のみにある」はかなり良いものです。通常、読者があなたの方法を読んでいるとき、彼らはその方法が99%の時間で何をするかを探しており、違法な入力やほとんど決して起こらない他のことを入力するユーザーを扱うような奇妙なコーナーケースで雑然としているのではありません。彼らは、あなたのソフトウェアの通常のフローが、問題が決して起こらないかのように、次々に直接配置されることを望んでいます。あなたのプログラムを理解することは、起こり得るあらゆる小さな問題に対処するために絶えず接線に立ち向かうことに対処する必要がないので、十分難しいでしょう。

2
Geo