チェック済みvsチェックなしvs例外なし…反対の信念のベストプラクティス


10

システムが例外を適切に伝達および処理するために必要な多くの要件があります。概念を実装するために言語を選択するための多くのオプションもあります。

例外の要件(順不同):

  1. ドキュメント:言語には、APIがスローできる例外をドキュメント化する手段が必要です。理想的には、このドキュメントメディアは、コンパイラとIDEがプログラマにサポートを提供できるようにマシンで使用できる必要があります。

  2. 例外的な状況の送信:これは明らかです。呼び出された機能が期待されるアクションを実行できない状況を関数が伝達できるようにするためです。私の意見では、そのような状況には3つの大きなカテゴリがあります。

    2.1一部のデータが無効になる原因となるコードのバグ。

    2.2構成またはその他の外部リソースの問題。

    2.3本質的に信頼できないリソース(ネットワーク、ファイルシステム、データベース、エンドユーザーなど)。これらは信頼できない性質のため、散発的な障害が発生する可能性があるため、ちょっとしたケースです。この場合、これらの状況は例外的と見なされますか?

  3. コードがそれを処理するための十分な情報を提供する:例外は、呼び出し先に対応し、状況を処理できるように、十分な情報を呼び出し先に提供する必要があります。ログに記録されたときに、この例外がプログラマーに問題のあるステートメントを識別して分離し、解決策を提供するのに十分なコンテキストを提供できるように、情報も十分でなければなりません。

  4. プログラマーにコードの実行状態の現在のステータスについて自信を与える:ソフトウェアシステムの例外処理機能は、プログラマーの邪魔にならないようにしながら必要な安全策を提供するために十分に存在している必要があります。手。

これらをカバーするために、以下のメソッドがさまざまな言語で実装されました。

  1. チェックされた例外は例外 を文書化する優れた方法を提供し、理論的には正しく実装された場合、すべてが良好であることを十分に保証するはずです。ただし、コストがかかるため、多くの場合、例外を飲み込むか、チェックされていない例外として再スローすることで単にバイパスする方が生産性が高くなります。不適切にチェックされた例外を使用すると、ほとんどすべての有用性が失われます。また、チェックされた例外は、時間的に安定したAPIの作成を困難にします。特定のドメイン内で汎用システムを実装すると、チェックされた例外のみを使用して維持することが困難になる例外的な状況が大量に発生します。

  2. チェックされていない例外 - チェックされた例外よりもはるかに用途が広く、特定の実装で起こり得る例外的な状況を適切に文書化できません。彼らは、仮にあったとしてもその場限りの文書に依存しています。これにより、メディアの信頼できない性質が、信頼性のある外観を提供するAPIによってマスクされます。また、これらの例外がスローされると、抽象化レイヤーを逆方向に移動するときに意味が失われます。それらは十分に文書化されていないため、プログラマーはそれらを明確に対象とすることができず、セカンダリシステムで障害が発生した場合にシステム全体を停止させないために、必要以上に広いネットをキャストする必要があります。これにより、提供された嚥下問題チェック例外に戻ることができます。

  3. マルチステートの戻り値の型 ここでは、予想外の結果または例外を表すオブジェクトを返すために、ばらばらのセット、タプル、またはその他の同様の概念に依存しています。ここでは、スタックの巻き戻し、コードのカットは行われません。すべてが正常に実行されますが、続行する前に戻り値のエラーを検証する必要があります。私はまだこれを実際に使用していませんので、経験からコメントすることはできません。通常のフローをバイパスしていくつかの問題の例外を解決しますが、チェックされた例外とほとんど同じ問題があり、面倒で常に「直面しています」。

だから問題は:

この問題についてのあなたの経験は何ですか、そしてあなたによると、言語が持つ優れた例外処理システムを作るための最良の候補は何ですか?


編集:この質問を書いてから数分後、私はこの投稿に出くわしました、不気味です!


2
「チェックされた例外と同じ問題があり、面倒で常に直面している」:本当にそうではない:適切な言語サポートがあれば、「成功パス」をプログラムするだけで、基礎となる言語機構が伝播を処理できます。エラー。
ジョルジオ

「言語には、APIがスローできる例外を文書化する手段が必要です。」 -weeeel。C ++で「私たち」は、これが実際には機能しないことを学びました。あなたはすべてのことができ、本当に有効に行うには、APIを投げることができるかどうかの状態にあるすべての例外を。(それは本当に長い話を短くしnoexceptますが、C ++で話を見ると、C#とJavaのEHについても非常に良い洞察が得られると思います。)
Martin Ba

回答:


10

C ++の初期の頃、ある種の汎用プログラミングがなければ、強く型付けされた言語は非常に扱いにくいことがわかりました。また、チェック例外とジェネリックプログラミングが一緒に機能しないことがわかりました。チェック例外は本質的に破棄されました。

マルチセットの戻り値の型は優れていますが、例外の代わりにはなりません。例外なく、コードはエラーチェックノイズでいっぱいです。

チェック例外のもう1つの問題は、低レベル関数によってスローされた例外が変更されると、すべての呼び出し元とその呼び出し元に一連の変更が強制されることです。これを防ぐ唯一の方法は、コードの各レベルで、下位レベルによってスローされた例外をキャッチし、それらを新しい例外にラップすることです。繰り返しになりますが、非常にノイズの多いコードになります。


2
ジェネリックスは、オブジェクト指向パラダイムに対する言語のサポートの制限が主な原因であるエラーのクラス全体を解決するのに役立ちます。それでも、代替手段は、ほとんどエラーチェックを行うコードを使用するか、何も問題が発生しないことを期待して実行されるコードを使用することです。あなたはいつも例外的な状況に直面している、または大きな悪いオオカミを真ん中に落とすと本当に醜く変わるふわふわの白いウサギの夢の土地に住んでいます!
ニュートピア

3
カスケード問題の+1。変更を困難にするシステム/アーキテクチャは、作者がどれほどうまく設計されていたとしても、モンキーパッチや厄介なシステムにつながるだけです。
Matthieu M.

2
@Newtopian:テンプレートは、厳密なオブジェクト指向では実行できないことを行います。たとえば、汎用コンテナーに静的な型安全性を提供します。
David Thornley、

2
「チェック済み例外」の概念を持つ例外システムが欲しいのですが、Javaとは非常に異なります。Checked-nessは、例外タイプの属性ではなく、スローサイト、キャッチサイト、例外インスタンスです。メソッドがチェック済み例外をスローするものとして宣伝されている場合、2つの効果があります。(1)関数は、戻り時に特別なことを行うことにより、チェック済み例外の「スロー」を処理する必要があります(例:キャリーフラグの設定など)。正確なプラットフォーム)呼び出しコードを準備する必要があります。
スーパーキャット2013年

7
「例外を除いて、コードにはエラーチェックノイズがたくさんあります。」:これについてはよくわかりません。Haskellではモナドを使用でき、すべてのエラーチェックノイズがなくなりました。「マルチステートの戻り値の型」によってもたらされるノイズは、ソリューション自体の制限というより、プログラミング言語の制限です。
ジョルジオ

9

長い間OO言語では、例外を使用することは、エラーを通信するための事実上の標準でした。しかし、関数型プログラミング言語は、異なるアプローチの可能性を提供します。たとえば、モナド(私は使用していません)を使用するか、Scott Wlaschinによって記述されているより軽量な「鉄道指向プログラミング」を使用します。

これは、実際にはマルチステート結果タイプのバリアントです。

  • 関数は成功またはエラーを返します。両方を返すことはできません(タプルの場合と同様)。
  • 発生する可能性のあるすべてのエラーが簡潔に文書化されています(少なくともF#では、識別された共用体としての結果の型を使用しています)。
  • 呼び出し元は、結果が成功か失敗かを考慮せずに結果を使用できません。

結果の型は次のように宣言できます

type Result<'TSuccess,'TFail> =
| Success of 'TSuccess
| Fail of 'TFail

したがって、このタイプを返す関数の結果は、SuccessまたはFailタイプのいずれかになります。両方にすることはできません。

より命令指向のプログラミング言語では、この種のスタイルでは、呼び出し元のサイトに大量のコードが必要になる可能性があります。ただし、関数型プログラミングでは、バインディング関数または演算子を作成して複数の関数を結び付けることができるため、エラーチェックでコードの半分を占めることはありません。例として:

// Create an updateUser function that takes an id, and new state
// as input, and updates an existing user.
let updateUser id input =
    validateInput input
    >>= loadUser id
    >>= updateUser input
    >>= saveUser id
    >>= notifyAboutUserUpdated

updateUser関数が連続してこれらの各関数を呼び出し、それらのそれぞれは、失敗する可能性があります。すべて成功すると、最後に呼び出された関数の結果が返されます。関数の1つが失敗した場合、その関数の結果はupdateUser関数全体の結果になります。これはすべてカスタム>> =演算子によって処理されます。

上記の例では、エラータイプは次のようになります。

type UserValidationErrorType =
| InvalidEmail of string
| MissingFirstName of string
... etc

type DbErrorType =
| RecordNotFound of int
| ConcurrencyError of int

type UpdateUserErrorType =
| InvalidInput of UserValidationErrorType
| DbError of DbErrorType

の呼び出し元がupdateUser関数からのすべての可能なエラーを明示的に処理しない場合、コンパイラーは警告を発行します。これで、すべてが文書化されました。

Haskellにはdo、コードをよりクリーンにするための表記法があります。


2
非常に良い答えと参照(鉄道指向プログラミング)、+ 1。Haskellのdo表記に言及すると、結果のコードがさらにきれいになります。
ジョルジオ

1
@ジョルジオ-私は今やったが、私はハスケルと一緒に仕事をしたことがなく、F#だけだったので、それについて本当にたくさん書くことができなかった。ただし、必要に応じて回答を追加できます。
ピート14年

ありがとう、私は小さな例を書きましたが、あなたの答えに追加するのに十分に小さくなかったので、完全な答えを書きました(いくつかの追加の背景情報を含む)。
ジョルジオ

2
これRailway Oriented Programmingはまさにモナディックな振る舞いです。
Daenyth、2016

5

私が見つけピートの答えは非常に良いと私はいくつかの考察と一例を追加したいと思います。例外の使用と特別なエラー値を返すことに関する非常に興味深い議論は、Robert Harper、Programming in Standard MLのセクション29.3、243ページ、244ページの最後にあります。

問題はf、あるタイプの値を返す部分関数を実装することですt。1つの解決策は、関数に型を持たせることです

f : ... -> t

可能な結果がない場合は例外をスローします。2番目の解決策は、次のタイプの関数を実装することです

f : ... -> t option

SOME v成功NONEしたら失敗して戻ります。

これは本からのテキストです。テキストをより一般的にするために自分で小さな変更を加えています(本は特定の例を参照しています)。変更されたテキストはイタリック体で書かれています

2つのソリューション間のトレードオフは何ですか?

  1. オプションのタイプに基づくソリューションは、関数fのタイプを明示的に失敗の可能性にします。これにより、プログラマーは、呼び出しの結果のケース分析を使用して明示的に失敗をテストする必要があります。型チェッカーは、期待されるt option場所でt使用できないことを保証し ます。例外に基づくソリューションは、そのタイプの失敗を明示的に示していません。ただし、それでもプログラマーは失敗の処理を余儀なくされます。それ以外の場合、キャッチされない例外エラーはコンパイル時ではなく実行時に発生します。
  2. オプションタイプに基づくソリューションでは、各呼び出しの結果について明示的なケース分析が必要です。「ほとんどの」結果が成功した場合、チェックは冗長であり、したがって非常にコストがかかります。例外に基づくソリューションにはこのオーバーヘッドがtありません。結果をまったく返さない「失敗」のケースではなく、を返す「通常」のケースに偏っています。例外の実装により、成功と比較して失敗がまれなケースでは、明示的なケース分析よりもハンドラーの使用がより効率的になります。

[カット]一般に、効率が最優先の場合、失敗が珍しい場合は例外を優先し、失敗が比較的一般的である場合はオプションを優先する傾向があります。一方、静的チェックが最も重要な場合は、型チェッカーが実行時にのみエラーが発生するのではなく、プログラマーがエラーをチェックするという要件を強制するため、オプションを使用する方が有利です。

これは、例外とオプションの戻り型の選択に関する限りです。

戻り値の型でエラーを表すと、エラーチェックがコード全体に広がるという考えに関して、これは当てはまる必要はありません。これを説明するHaskellの小さな例を次に示します。

2つの数値を解析してから、最初の数値を2番目の数値で除算するとします。したがって、各数値の解析中、または除算(ゼロによる除算)中にエラーが発生する可能性があります。したがって、各ステップの後にエラーをチェックする必要があります。

import Text.Read

parseInt :: String -> Maybe Int
parseInt s = readMaybe s :: Maybe Int

safeDiv :: Int -> Int -> Maybe Int
safeDiv n d = if d /= 0 then Just (n `div` d) else Nothing

toString :: Maybe Int -> String
toString (Just i) = show i
toString Nothing  = "error"

main = do
         -- Get two lines from the terminal.
         nStr <- getLine
         dStr <- getLine

         -- Parse each string and divide.
         let r = do n <- parseInt nStr
                    d <- parseInt dStr
                    safeDiv n d

         -- Print the result.
         putStrLn $ toString r

解析と除算はlet ...ブロックで実行されます。Maybeモナドとdo表記法を使用することにより、成功パスのみが指定されることに注意してください。モナドのセマンティクスはMaybe、エラー値(Nothing)を暗黙的に伝播します。プログラマーのオーバーヘッドはありません。


2
このような場合に、何らかの有用なエラーメッセージを出力しEitherたい場合は、タイプの方が適していると思います。Nothingここに来たらどうしますか?「エラー」というメッセージが表示されるだけです。デバッグにはあまり役立ちません。
サラ2016年

1

私はチェック例外の大ファンになりました。いつチェック例外を使用するかについての私の一般的なルールを共有したいと思います。

私のコードが処理しなければならないエラーには基本的に2つのタイプがあるという結論に達しました。コードの実行前にテスト可能なエラーと、コードの実行前にテスト不可能なエラーがあります。コードがNullPointerExceptionで実行される前にテスト可能なエラーの簡単な例。

//... bad code below.  the runnable variable
// tries to call the run() method before the variable
// is instantiated.  Running the code below will cause
// a NullPointerException.
Runnable runnable = null;
runnable.run();

簡単なテストで次のようなエラーを回避できたはずです...

Runnable runnable = null;
...
if (runnable != null)
{   runnable.run(); }

コンピューティングでは、コードを実行する前に1つ以上のテストを実行して安全であることを確認し、例外が発生する場合があります。たとえば、ファイルシステムをテストして、データをドライブに書き込む前に、ハードドライブに十分なディスク領域があることを確認できます。今日使用されているようなマルチプロセッシングオペレーティングシステムでは、プロセスがディスク領域をテストし、ファイルシステムが十分な領域があることを示す値を返し、別のプロセスへのコンテキストスイッチがオペレーティングシステムで使用可能な残りのバイトを書き込む可能性があります。システム。オペレーティングシステムコンテキストが、コンテンツをディスクに書き込む実行中のプロセスに戻ると、ファイルシステムに十分なディスク領域がないために例外が発生します。

上記のシナリオは、チェック例外の完璧なケースと考えています。これは、コードが完全に記述されていても、何か悪いことに対処することを強制するコードの例外です。「例外を飲み込む」などの悪いことをするなら、あなたは悪いプログラマです。ちなみに、例外を飲み込むのが妥当な場合もありますが、例外が飲み込まれた理由についてコードにコメントを残してください。例外処理メカニズムは責任を負いません。私はよく心臓のペースメーカーがチェック例外を備えた言語で書かれることを望んでいると冗談を言っています。

コードがテスト可能かどうかを判断することが困難になる場合があります。たとえば、インタープリターを作成していて、構文上の理由でコードの実行に失敗したときにSyntaxExceptionがスローされる場合、SyntaxExceptionはチェック済み例外にするか、(Javaの場合)ランタイム例外にする必要がありますか?コードが実行される前にインタープリターがコードの構文をチェックする場合、例外はRuntimeExceptionになるはずです。インタープリターがコードを「ホット」に実行し、構文エラーにヒットするだけの場合、例外はチェック例外でなければなりません。

何をすべきかわからないときがあるので、チェック例外をキャッチしたりスローしたりしなければならないことがいつも嬉しいわけではないことを認めます。チェック例外は、プログラマに発生する可能性のある潜在的な問題を意識させる方法です。私がJavaでプログラミングする理由の1つは、チェックされた例外があるためです。


1
私の心臓ペースメーカーは例外がまったくない言語で書かれていて、コードのすべての行がリターンコードを介してエラーを処理していました。例外をスローすると、「すべてがうまくいかなくなった」と言い、処理を続行する唯一の安全な方法は、停止して再起動することです。非常に簡単に無効な状態になるプログラムは、重要なソフトウェアに必要なものではありません(JavaはEULAでの重要なソフトウェアの使用を明示的に許可していません)
gbjbaanb 2015年

例外を使用してそれらをチェックしないvsリターンコードを使用して最終的にそれらをチェックしないと、すべて同じ心停止になります。
ニュートピア

-1

私は現在、かなり大規模なOOPベースのプロジェクト/ APIの真ん中にあり、この例外のレイアウトを使用しています。しかし、それはすべて、例外処理などをどの程度深くしたいかによります。

ExpectedException
-AuthorisedException
-EmptySetException
-NoRemainingException
-NoRowsException
-NotFoundException
-ValidationException

UnexpectedException
-ConnectivityException
-EnvironmentException
-ProgrammerException
-SQLException

   $valid_types = array('mysql', 'oracle', 'sqlite');
       if (!in_array($type, $valid_types)) {
           throw new ecProgrammerException(
        'The database type specified, %1$s, is invalid. Must be one of: %2$s.',
    $type,
    join(', ', $valid_types)
    );
}

11
例外が予想される場合、それは実際には例外ではありません。「NoRowsException」?制御フローのように聞こえるので、例外の使用は不十分です。
クエンティンスターリン

1
@qes:double Math.sqrt(double v)やUser findUser(long id)などの関数が値を計算できない場合は常に例外を発生させるのが理にかなっています。これにより、呼び出しのたびにチェックする代わりに、呼び出し側が自由にエラーをキャッチして処理できるようになります。
ケビンクライン

1
予期される=制御フロー=例外のアンチパターン。例外を制御フローに使用しないでください。特定の入力でエラーが発生すると予想される場合は、戻り値の一部として渡されます。したがって、NANまたはがありNULLます。
Eonil、2013

1
@Eonil ...またはOption <T>
Maarten Bodewes 14
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.