アサーションまたは例外を使用して契約に従って設計しますか?[閉まっている]
-
02-07-2019 - |
質問
契約に基づいてプログラミングする場合、関数またはメソッドは、その責任を果たし始める前に、まず前提条件が満たされているかどうかをチェックします。これらのチェックを行う最も有名な 2 つの方法は次のとおりです。 assert
そしてによって exception
.
- アサートはデバッグ モードでのみ失敗します。確実にするには、個別の契約の前提条件をすべて (単体) テストして、実際に失敗するかどうかを確認することが重要です。
- デバッグ モードとリリース モードでは例外が失敗します。これには、テストされたデバッグ動作がリリース動作と同じであるという利点がありますが、実行時のパフォーマンスが低下します。
どちらが好ましいと思いますか?
関連する質問を参照してください ここ
解決
リリース ビルドでアサートを無効にすることは、「リリース ビルドでは一切問題が発生しません」と言っているようなものですが、実際はそうではないことがよくあります。したがって、リリース ビルドではアサートを無効にするべきではありません。しかし、エラーが発生するたびにリリース ビルドがクラッシュすることも望ましくありません。
したがって、例外を使用してうまく活用してください。適切で堅牢な例外階層を使用し、確実にキャッチできるようにします。デバッガで例外スローにフックを設定してキャッチすることができます。また、リリース モードでは、直接クラッシュするのではなく、エラーを補うことができます。それがより安全な方法です。
他のヒント
経験則として、自分自身のエラーを検出する場合はアサーションを使用し、他の人のエラーを検出する場合は例外を使用する必要があります。つまり、システムの外部にあるデータを取得するときは常に、例外を使用してパブリック API 関数の前提条件をチェックする必要があります。システムの内部にある関数またはデータにはアサートを使用する必要があります。
私が従う原則は次のとおりです。コーディングによって状況を現実的に回避できる場合は、アサーションを使用します。それ以外の場合は例外を使用してください。
アサーションは、契約が遵守されていることを確認するためのものです。契約は公正でなければならないため、クライアントは契約を確実に遵守できる立場になければなりません。たとえば、有効な URL と無効な URL に関するルールは既知であり、一貫しているため、URL は有効でなければならないと契約に記載できます。
例外は、クライアントとサーバーの両方の制御の外にある状況です。例外とは、何か問題が発生し、それを回避する方法がなかったことを意味します。たとえば、ネットワーク接続はアプリケーションの制御の外にあるため、ネットワーク エラーを回避するためにできることは何もありません。
アサーションと例外の区別は、実際には最良の考え方ではないことを付け加えておきたいと思います。本当に考えておきたいのは、契約とそれをどのように執行できるかということです。上記の URL の例では、URL をカプセル化する、Null または有効な URL のいずれかのクラスを作成することが最善です。コントラクトを強制するのは文字列から URL への変換であり、それが無効な場合は例外がスローされます。URL パラメーターを持つメソッドは、String パラメーターと URL を指定するアサーションを持つメソッドよりもはるかに明確です。
アサートは、開発者 (自分だけでなく、チームの別の開発者も) が間違ったことをしたことを発見するために使用されます。ユーザーのミスによってこの状況が発生する可能性があることが合理的である場合は、例外とする必要があります。
同様に結果についても考えてください。通常、アサートによりアプリがシャットダウンされます。状態を回復できるという現実的な期待がある場合は、おそらく例外を使用する必要があります。
一方、問題が解決できる場合は、 のみ プログラマ エラーが原因である場合は、できるだけ早くそれを知りたいため、アサートを使用します。例外が捕捉されて処理される可能性がありますが、それを知ることは決してありません。そして、はい、リリースコードでアサートを無効にする必要があります。これは、わずかな可能性がある場合にアプリが回復するようにするためです。たとえプログラムの状態が大きく壊れていたとしても、ユーザーは作業内容を保存できる可能性があります。
「デバッグ モードでのみアサートが失敗する」というのは正確には真実ではありません。
で オブジェクト指向ソフトウェア構築、第 2 版 Bertrand Meyer によると、著者はリリース モードで前提条件を確認するためのドアを開いたままにしています。その場合、アサーションが失敗すると何が起こるかというと…アサーション違反例外が発生しました。この場合、状況から回復することはできません。ただし、エラー レポートを自動的に生成し、場合によってはアプリケーションを再起動するなど、何か便利なことができるかもしれません。
この背後にある動機は、通常、前提条件は不変条件や事後条件よりもテストコストが安く、場合によってはリリース ビルドの正確性と「安全性」が速度よりも重要であるためです。つまり多くのアプリケーションでは速度は問題になりませんが、 堅牢性 (プログラムの動作が正しくない場合に、安全な方法で動作するプログラムの能力。契約が破棄されたとき)です。
前提条件チェックは常に有効のままにしておく必要がありますか?場合によります。それはあなた次第です。普遍的な答えはありません。銀行用のソフトウェアを作成している場合は、1,000 ドルではなく 1,000,000 ドルを送金するよりも、警告メッセージを表示して実行を中断する方が良いかもしれません。しかし、ゲームをプログラミングしている場合はどうなるでしょうか?おそらく、得られるすべてのスピードが必要であり、前提条件が捕捉できなかったバグ (有効になっていないため) のせいで誰かが 10 ポイントではなく 1000 ポイントを獲得した場合は、運が悪いでしょう。
どちらの場合も、理想的にはテスト中にそのバグを発見し、テストの重要な部分をアサーションを有効にして実行する必要があります。ここで議論されているのは、不完全なテストのために以前に検出されなかったシナリオで、実稼働コードで前提条件が失敗するというまれなケースに対する最適なポリシーは何かということです。
要約する、 アサーションを持っていても例外を自動的に取得できます, 有効のままにしておくと、少なくともエッフェルでは。C++ で同じことを行うには、自分で入力する必要があると思います。
以下も参照してください。 アサーションを実稼働コードに残す必要があるのはどのような場合ですか?
巨大なものがありました 糸 comp.lang.c++.moderated のリリース ビルドでのアサーションの有効化/無効化については、数週間あれば、これに関する意見がいかに多様であるかがわかるでしょう。:)
に反して コプロ, リリース ビルドでアサーションを無効にできるかどうかわからない場合は、アサーションを無効にするべきではないと思います。アサーションは、プログラムの不変条件が破られるのを防ぐためのものです。このような場合、コードのクライアントに関する限り、考えられる結果は 2 つのうちいずれかになります。
- ある種の OS タイプの障害により終了し、中止の呼び出しが発生します。(アサートなし)
- 中止するための直接呼び出しを介して終了します。(アサートあり)
ユーザーにとっては違いはありませんが、コードが失敗しないほとんどの実行では、アサーションによってコードに不必要なパフォーマンス コストが追加される可能性があります。
この質問に対する答えは、実際には、API のクライアントが誰になるかによって大きく異なります。API を提供するライブラリを作成している場合は、顧客が API を誤って使用したことを通知する何らかのメカニズムが必要です。ライブラリの 2 つのバージョン (1 つはアサートあり、もう 1 つはアサートなし) を提供しない限り、assert が適切な選択である可能性はほとんどありません。
ただし、個人的には、この場合も例外を認めるかどうかはわかりません。例外は、適切な形式のリカバリが実行できる場合に適しています。たとえば、メモリを割り当てようとしている可能性があります。「std::bad_alloc」例外をキャッチした場合は、メモリを解放して再試行できる可能性があります。
私はこの問題の状況についての私の見解をここで概説しました: オブジェクトの内部状態をどのように検証するのでしょうか? 。一般に、自分の主張を主張し、他人の違反を非難します。リリース ビルドでアサートを無効にするには、次の操作を実行できます。
- 負荷の高いチェック (範囲が順序付けされているかどうかのチェックなど) のアサートを無効にする
- 簡単なチェックを有効にしておく (null ポインタやブール値のチェックなど)
もちろん、リリース ビルドでは、失敗したアサーションとキャッチされなかった例外は、デバッグ ビルド (std::abort を呼び出すだけ) とは別の方法で処理する必要があります。エラーのログをどこか (おそらくファイル) に書き込み、内部エラーが発生したことを顧客に伝えます。顧客はログファイルを送信することができます。
あなたは設計時エラーと実行時エラーの違いについて質問しています。
アサートは「プログラマーさん、これは壊れています」という通知であり、発生時には気づかなかったバグを思い出させるためにあります。
例外は、「ユーザーさん、何か問題が発生しました」という通知です (もちろん、ユーザーに通知されないように、通知をキャッチするコードを作成できます) が、これらは、Joe ユーザーがアプリを使用しているときに実行時に発生するように設計されています。
したがって、すべてのバグを取り除くことができると考えている場合は、例外のみを使用してください。無理だと思ったら……例外を使用します。もちろん、デバッグ アサートを使用して例外の数を減らすこともできます。
前提条件の多くはユーザーが提供したデータであることを忘れないでください。そのため、ユーザーにデータが不適切であることを通知する適切な方法が必要になります。そのためには、多くの場合、呼び出しスタックの下で、対話しているビットにエラー データを返す必要があります。その場合、アサートは役に立ちません。アプリが n 層の場合は、さらに役に立ちません。
最後に、私はどちらも使用しません。定期的に発生すると思われるエラーに対しては、エラー コードの方がはるかに優れています。:)
私は2番目の方が好きです。テストは正常に実行されたかもしれませんが、 マーフィー 予期せぬことが起こるだろうと言います。したがって、実際の誤ったメソッド呼び出しで例外を取得する代わりに、10 スタック フレームの深さの NullPointerException (または同等の) をトレースすることになります。
以前の答えは正しいです:パブリック API 関数には例外を使用します。このルールを曲げた方がよいのは、チェックの計算コストが高い場合だけです。その場合、あなたは、 できる それをアサートに入れます。
その前提条件に違反する可能性が高いと思われる場合は、それを例外として保持するか、前提条件をリファクタリングして取り除きます。
両方を使用する必要があります。アサートは開発者の利便性を目的としています。例外は、実行時に見逃したものや予期しなかったものをキャッチします。
好きになってしまった glib のエラー報告関数 単純な古い主張の代わりに。これらは Assert ステートメントのように動作しますが、プログラムを停止するのではなく、単に値を返してプログラムを続行させます。これは驚くほどうまく機能し、おまけに、関数が「本来のもの」を返さなかった場合にプログラムの残りの部分に何が起こるかを確認することができます。クラッシュした場合は、どこかでエラーチェックが緩んでいることがわかります。
前回のプロジェクトでは、これらのスタイルの関数を使用して前提条件チェックを実装し、そのうちの 1 つが失敗した場合はスタック トレースをログ ファイルに出力しましたが、実行は継続しました。私のデバッグ ビルドを実行するときに他の人が問題に遭遇する可能性があるため、デバッグ時間を大幅に節約できました。
#ifdef DEBUG
#define RETURN_IF_FAIL(expr) do { \
if (!(expr)) \
{ \
fprintf(stderr, \
"file %s: line %d (%s): precondition `%s' failed.", \
__FILE__, \
__LINE__, \
__PRETTY_FUNCTION__, \
#expr); \
::print_stack_trace(2); \
return; \
}; } while(0)
#define RETURN_VAL_IF_FAIL(expr, val) do { \
if (!(expr)) \
{ \
fprintf(stderr, \
"file %s: line %d (%s): precondition `%s' failed.", \
__FILE__, \
__LINE__, \
__PRETTY_FUNCTION__, \
#expr); \
::print_stack_trace(2); \
return val; \
}; } while(0)
#else
#define RETURN_IF_FAIL(expr)
#define RETURN_VAL_IF_FAIL(expr, val)
#endif
実行時に引数をチェックする必要がある場合は、次のようにします。
char *doSomething(char *ptr)
{
RETURN_VAL_IF_FAIL(ptr != NULL, NULL); // same as assert(ptr != NULL), but returns NULL if it fails.
// Goes away when debug off.
if( ptr != NULL )
{
...
}
return ptr;
}
ここで他の回答のいくつかを私自身の見解と統合してみました。
アサーションを運用環境で無効にし、アサーションを残したままにしておきたい場合には、アサーションを使用します。開発ではなく運用環境で無効にする唯一の本当の理由は、プログラムを高速化することです。ほとんどの場合、この高速化は大幅ではありませんが、コードがタイムクリティカルである場合や、テストの計算コストが高くつく場合があります。コードがミッションクリティカルである場合は、速度が遅くなっても例外が最適である可能性があります。
実際に回復できる可能性がある場合は、アサーションは回復するように設計されていないため、例外を使用してください。たとえば、コードがプログラミング エラーから回復するように設計されていることはほとんどありませんが、ネットワーク障害やロックされたファイルなどの要因から回復するように設計されています。エラーは、プログラマの制御の範囲外であるという理由だけで例外として処理されるべきではありません。むしろ、コーディングミスに比べて、これらのエラーは予測可能であるため、リカバリが容易になります。
アサーションのデバッグが簡単であるという議論については、次のようになります。適切に名前が付けられた例外からのスタック トレースは、アサーションと同じくらい読みやすいです。優れたコードは、特定の種類の例外のみをキャッチする必要があるため、キャッチされたために例外が見逃されることはありません。ただし、Java ではすべての例外をキャッチすることが強制される場合があると思います。
こちらも参照 この質問:
場合によっては、リリース用にビルドするときにアサートが無効になることがあります。これを制御できない場合があります(そうでなければ、アサートを使用してビルドできます)。そのため、このようにすることをお勧めします。
入力値を「修正」することの問題は、発信者が予想されるものを取得しないことであり、これはプログラムの完全に異なる部分で問題につながり、さらには悪夢のようにデバッグされる可能性があることです。
私は通常、IF-Statementに例外を投げかけて、それらが無効になっている場合にアサートの役割を引き継ぐ
assert(value>0); if(value<=0) throw new ArgumentOutOfRangeException("value"); //do stuff
私の経験則では、内部エラーと外部エラーの例外を見つけるには、assert 式を使用します。Greg による次の議論から多くの利益を得ることができます。 ここ.
Assert 式はプログラミング エラーを見つけるために使用されます。プログラムのロジック自体のエラー、または対応する実装のエラーのいずれかです。アサート条件は、プログラムが定義された状態に留まっていることを検証します。「定義された状態」とは、基本的にはプログラムの前提条件と一致する状態です。プログラムの「定義された状態」は、「理想的な状態」、「通常の状態」、さらには「有用な状態」である必要はないことに注意してください。この重要な点については後ほど説明します。
アサーションがプログラムにどのように適合するかを理解するには、ポインターを繰り返しようとしているC ++プログラムのルーチンを検討してください。ここで、定期的なポインターが控えめであるかどうかをテストする必要がありますか、それともポインターがnullではないことを主張してから、それを前に進めて繰り返しますか?
ほとんどの開発者は両方を行い、アサートを追加したいだけでなく、主張された条件が失敗した場合にクラッシュしないように、ヌル値をポインターに確認することを望んでいると思います。表面的には、テストとチェックの両方を実行することは、最も賢明な決定のように思えるかもしれません
主張された条件とは異なり、プログラムのエラー処理(例外)とは、プログラムのエラーではなく、環境から取得したプログラムを入力することを指します。これらは、パスワードを入力せずにアカウントにログインしようとするユーザーなど、誰かの側の「エラー」であることがよくあります。また、エラーによりプログラムのタスクが正常に完了することを妨げる可能性がありますが、プログラムの障害はありません。プログラムは、外部エラーのためにパスワードなしでユーザーにログインすることができません - ユーザー部分のエラー。状況が異なっていて、ユーザーが正しいパスワードを入力し、プログラムがそれを認識できなかった場合。その後、結果は依然として同じですが、失敗はプログラムに属します。
エラー処理 (例外) の目的は 2 つあります。1つ目は、プログラムの入力のエラーが検出され、それが何を意味するかをユーザー(または他のクライアント)に通信することです。2番目の目的は、エラーが検出された後、明確に定義された状態にアプリケーションを復元することです。この状況では、プログラム自体が誤っていないことに注意してください。確かに、このプログラムは非理想的な状態、または役に立たない状態でさえあるかもしれませんが、プログラミングエラーはありません。それどころか、エラー回復状態はプログラムの設計によって予想されるものであるため、プログラムが処理できるものがISになります。
追伸:同様の質問をチェックしてみてください。 例外とアサーション.