web-dev-qa-db-ja.com

アサーションまたは例外を使用した契約による設計?

契約によるプログラミングの場合、関数またはメソッドは、その前提条件が満たされているかどうかを最初にチェックしてから、その責務に取り組み始めますか?これらのチェックを行うための2つの最も顕著な方法は、assertexceptionによるものです。

  1. assertはデバッグモードでのみ失敗します。すべての個別の契約前提条件を(ユニット)テストして、実際に失敗するかどうかを確認することが重要です。
  2. デバッグおよびリリースモードで例外が失敗します。これには、テストされたデバッグ動作がリリース動作と同一であるという利点がありますが、実行時のパフォーマンスが低下します。

どちらが望ましいと思いますか?

関連する質問を参照してください こちら

119
andreas buykx

リリースビルドでアサートを無効にすることは、「リリースビルドで問題が発生することは決してありません」と言っているようなものですが、多くの場合そうではありません。したがって、リリースビルドでassertを無効にしないでください。しかし、エラーが発生するたびにリリースビルドがクラッシュするのは望ましくありませんか?

したがって、例外を使用し、それらを適切に使用してください。優れた堅実な例外階層を使用し、キャッチすることを確認し、デバッガーでスローする例外にフックをかけてキャッチします。リリースモードでは、ストレートクラッシュではなくエラーを補正できます。より安全な方法です。

39
coppro

経験則では、自分のエラーをキャッチしようとするときはアサーションを使用し、他の人のエラーをキャッチしようとするときは例外を使用する必要があります。つまり、例外を使用して、パブリックAPI関数の前提条件を確認し、システムの外部にあるデータを取得するたびに確認する必要があります。システム内部の関数またはデータに対してアサートを使用する必要があります。

194
Dima

私が従う原則はこれです:コーディングによって状況を現実的に回避できる場合は、アサーションを使用します。それ以外の場合は、例外を使用します。

アサーションは、契約が遵守されていることを確認するためのものです。契約は公正である必要があり、クライアントはそれを確実に順守する必要があります。たとえば、有効なURLであるものとそうでないものに関するルールが既知で一貫しているため、URLが有効でなければならないことを契約に記載できます。

例外は、クライアントとサーバーの両方の制御外の状況です。例外とは、何かが間違っていることを意味し、それを回避するためにできることは何もありません。たとえば、ネットワーク接続はアプリケーションの制御外にあるため、ネットワークエラーを回避するためにできることは何もありません。

アサーション/例外の区別は、それについて考える最良の方法ではないことを付け加えておきます。あなたが本当に考えたいのは、契約とその実施方法です。上記のURLの例では、URLをカプセル化し、Nullまたは有効なURLであるクラスを作成するのが最善です。契約を実施するのは文字列をURLに変換することであり、無効な場合は例外がスローされます。 URLパラメーターを使用したメソッドは、StringパラメーターとURLを指定するアサーションを使用したメソッドよりもはるかに明確です。

22
Ged Byrne

アサーションは、開発者が間違ったことをしたことをキャッチするためのものです(自分自身だけでなく、チームの他の開発者も)。ユーザーのミスによってこの状態が発生する可能性がある場合、それは例外です。

同様に、結果についても考えてください。通常、アサートはアプリをシャットダウンします。状態を回復できるという現実的な期待がある場合は、おそらく例外を使用する必要があります。

一方、問題がonlyである可能性がある場合は、できるだけ早く知る必要があるため、アサートを使用します。 。例外がキャッチされて処理される可能性がありますが、それについて知ることは決してありません。はい。リリースコードでアサートを無効にする必要があります。これは、わずかな可能性がある場合にアプリを回復させるためです。プログラムの状態が非常に壊れていても、ユーザーは作業を保存できる可能性があります。

6
DJClayworth

「デバッグモードでのみアサートが失敗する」というのは正確には真実ではありません。

Bertrand MeyerによるObject Oriented Software Construction、2nd Editionでは、著者はリリースモードで前提条件をチェックするための扉を開いたままにします。その場合、アサーションが失敗したときに起こることは...アサーション違反例外が発生することです!この場合、状況からの回復はありません。しかし、何か役に立つことができます。また、エラーレポートを自動的に生成し、場合によってはアプリケーションを再起動することもできます。

この背後にある動機は、不変条件と事後条件よりも一般に前提条件の方がテストが安価であり、場合によってはリリースビルドの正確さと「安全性」が速度よりも重要であることです。すなわち、多くのアプリケーションでは速度は問題ではありませんが、robustness(その動作が正しくない場合にプログラムが安全に動作する能力、すなわち契約が破られたとき)です。

前提条件チェックを常に有効にしておく必要がありますか?場合によります。それはあなた次第です。普遍的な答えはありません。銀行向けのソフトウェアを作成している場合は、1,000ドルではなく1,000,000ドルを送金するよりも、警告メッセージで実行を中断する方がよい場合があります。しかし、ゲームをプログラミングしている場合はどうでしょうか?たぶん、あなたはあなたが得ることができるすべての速度を必要とします、そして、誰かが前提条件が捕らえられなかったバグのために10ポイントの代わりに1000ポイントを得るなら(それらは有効にされていないので)、不運です。

どちらの場合も、理想的にはテスト中にそのバグをキャッチし、アサーションを有効にしてテストの大部分を実行する必要があります。ここで説明しているのは、テストが不完全であるために以前に検出されなかったシナリオで、本番コードで前提条件が失敗するというまれなケースに対する最善のポリシーです。

要約すると、アサーションがあり、例外を有効のままにしておくと、少なくともエッフェルでは例外を自動的に取得できます。私はあなたがあなた自身でそれをタイプする必要があるC++で同じことをすると思います。

参照: アサーションはいつプロダクションコードに残るべきですか?

5
Daniel Daranas

ここで問題の状態に関する私の見解を概説しました: オブジェクトの内部状態をどのように検証しますか? 。一般的に、あなたの主張を主張し、他人による違反をスローします。リリースビルドでアサートを無効にするには、次のようにします。

  • 高価なチェック(範囲が順序付けられているかどうかのチェックなど)のアサートを無効にします
  • 簡単なチェックを有効にしておく(nullポインターやブール値のチェックなど)

もちろん、リリースビルドでは、失敗したアサーションとキャッチされない例外は、デバッグビルド(std :: abortを呼び出すだけの場合)とは別の方法で処理する必要があります。エラーのログをどこかに(おそらくファイルに)書き、内部エラーが発生したことをお客様に伝えます。顧客はあなたにログファイルを送ることができます。

Comp.lang.c ++。moderatedのリリースビルドでのアサーションの有効化/無効化に関して、大きな thread がありました。数週間あれば、これに対する意見の多様性を確認できます。 :)

coppro に反して、リリースビルドでアサーションを無効にできるかどうかわからない場合、アサーションではないはずです。アサーションは、プログラムの不変条件が破られるのを防ぐためのものです。このような場合、コードのクライアントに関する限り、次の2つの結果のいずれかがあります。

  1. 何らかのOSタイプの障害で死に、その結​​果、中止の呼び出しが発生します。 (アサートなし)
  2. 中止する直接呼び出しを介してダイ。 (アサートあり)

ユーザーに違いはありませんが、アサーションにより、コードが失敗しない実行の大部分に存在するコードに不要なパフォーマンスコストが追加される可能性があります。

質問に対する答えは、実際には、APIのクライアントが誰であるかによります。 APIを提供するライブラリを作成している場合は、APIを誤って使用したことを顧客に通知する何らかの形式のメカニズムが必要です。ライブラリの2つのバージョン(1つはアサートあり、もう1つはアサートなし)を指定しない限り、アサートが適切な選択となることはほとんどありません。

しかし、個人的には、この場合にも例外があるかどうかはわかりません。例外は、適切な形式の回復を実行できる場所により適しています。たとえば、メモリを割り当てようとしている可能性があります。 「std :: bad_alloc」例外をキャッチすると、メモリを解放して再試行できる場合があります。

2
Richard Corden

設計時エラーと実行時エラーの違いについて尋ねています。

アサートは「ちょっとプログラマー、これは壊れています」という通知であり、発生したときに気付かなかったバグを思い出させるためにあります。

例外は「ちょっとユーザー、何かがおかしい」という通知です(もちろん、ユーザーに通知されないようにそれらをキャッチするコードを作成できます)が、これらはジョーユーザーがアプリを使用している実行時に発生するように設計されています。

したがって、すべてのバグを解決できると思われる場合は、例外のみを使用してください。できないと思うなら.....例外を使用してください。もちろん、デバッグアサートを使用して、例外の数を減らすことができます。

前提条件の多くがユーザー指定のデータになることを忘れないでください。そのため、ユーザーに自分のデータが良くないことを知らせる良い方法が必要になります。これを行うには、多くの場合、エラーデータをコールスタックから彼がやり取りしているビットに戻す必要があります。その場合、アサーションは役に立たなくなります-アプリがn層の場合は二重です。

最後に、私はどちらも使用しません-エラーコードは、定期的に発生すると思われるエラーに対してはるかに優れています。 :)

1
gbjbaanb

ここで、他のいくつかの回答を自分の見解で統合してみました。

本番環境で無効にしたい場合は、アサーションを使用します。アサーションはそのままにしておきます。本番環境で無効にする唯一の本当の理由は、開発ではなく、プログラムの高速化です。ほとんどの場合、この速度の向上はそれほど重要ではありませんが、コードが時間的に重要であるか、テストに計算コストがかかる場合があります。コードがミッションクリティカルである場合、スローダウンにもかかわらず例外が最適になる場合があります。

回復の本当の可能性がある場合、アサーションは回復されるように設計されていないため、例外を使用します。たとえば、コードがプログラミングエラーから回復するように設計されていることはめったにありませんが、ネットワーク障害やロックされたファイルなどの要因から回復するように設計されています。エラーは、プログラマーの制御外にあるという理由だけで例外として処理されるべきではありません。むしろ、これらのエラーの予測可能性は、コーディングの間違いと比較して、回復しやすくなっています。

アサーションをデバッグする方が簡単だという議論に再注意してください。適切な名前の例外からのスタックトレースは、アサーションと同じくらい読みやすいです。適切なコードは特定の種類の例外のみをキャッチする必要があるため、例外はキャッチされても気付かれることはありません。ただし、Javaは、すべての例外をキャッチすることを強制する場合があります。

0
Casebash

私は2番目のものを好む。テストは正常に実行された可能性がありますが、 Murphy は、予期しない何かが失敗することを示しています。そのため、実際のエラーのあるメソッド呼び出しで例外を取得する代わりに、NullPointerException(または同等の)10スタックフレーム深くトレースすることになります。

0
jdmichal

前の答えは正しいです。パブリックAPI関数には例外を使用します。このルールを曲げることができるのは、チェックの計算コストが高い場合のみです。その場合、あなたはcanをアサートに入れます。

その前提条件に違反する可能性が高いと思われる場合は、例外として保持するか、前提条件をリファクタリングします。

0
Mike Elkins

この質問 も参照してください:

場合によっては、リリース用にビルドするときにアサートが無効になります。これを制御できない場合があります(そうでない場合は、アサートをオンにしてビルドできます)。したがって、このようにすることをお勧めします。

入力値を「修正」する際の問題は、呼び出し元が期待するものを取得できないことであり、これは問題を引き起こしたり、プログラムのまったく異なる部分でクラッシュしたりする可能性があり、デバッグが悪夢になります。

私は通常、ifステートメントで例外をスローして、無効になっている場合にアサートの役割を引き継ぎます

assert(value>0);
if(value<=0) throw new ArgumentOutOfRangeException("value");
//do stuff
0
Rik

私の経験則では、アサート式を使用して、内部エラーと外部エラーの例外を見つけます。 here のGregによる次の議論から多くの利益を得ることができます。

アサート式は、プログラミングエラー(プログラムのロジック自体のエラーまたは対応する実装のエラー)を見つけるために使用されます。アサート条件は、プログラムが定義された状態のままであることを確認します。 「定義された状態」とは、基本的にプログラムの仮定に同意する状態です。プログラムの「定義された状態」は、「理想的な状態」、「通常の状態」、「有用な状態」である必要はなく、その重要な点については後で詳しく説明します。

アサーションがプログラムにどのように適合するかを理解するには、ポインターを間接参照しようとしているC++プログラムのルーチンを検討してください。ここで、ルーチンは、参照解除の前にポインターがNULLであるかどうかをテストする必要がありますか、それともポインターがNULLではないことをアサートしてから、それとは無関係に参照解除する必要がありますか?

ほとんどの開発者は、両方を実行し、アサートを追加するだけでなく、アサートされた条件が失敗した場合にクラッシュしないように、NULL値のポインターもチェックすることを想像します。表面的には、テストとチェックの両方を実行することが最も賢明な決定に思えるかもしれません

アサートされた条件とは異なり、プログラムのエラー処理(例外)は、プログラム内のエラーではなく、プログラムが環境から取得する入力を指します。これらは多くの場合、ユーザーがパスワードを入力せずにアカウントにログインしようとするなど、誰かの「エラー」です。また、エラーがプログラムのタスクの正常な完了を妨げる場合がありますが、プログラムの障害はありません。プログラムは、外部エラー-ユーザー側のエラーのため、パスワードなしでユーザーにログインできません。状況が異なり、ユーザーが正しいパスワードを入力したが、プログラムがそれを認識できなかった場合。その場合、結果は同じままですが、失敗はプログラムに属します。

エラー処理(例外)の目的は2つあります。 1つは、プログラムの入力でエラーが検出されたこととその意味をユーザー(または他のクライアント)に伝えることです。 2番目の目的は、エラーが検出された後、明確に定義された状態にアプリケーションを復元することです。この状況では、プログラム自体はエラーではないことに注意してください。確かに、プログラムは理想的でない状態、または有用なことは何もできないが、プログラミングエラーはない状態である可能性があります。それどころか、エラー回復状態はプログラムの設計によって予期される状態であるため、プログラムが処理できる状態です。

PS:同様の質問 Exception Vs Assertion を確認してください。

0
herohuyongtao

両方を使用する必要があります。アサートは、開発者としての利便性のためです。例外は、実行時に見逃した、または予期していなかったものをキャッチします。

私は昔の単純な主張の代わりに glibのエラー報告関数 が好きになりました。これらはアサート文のように動作しますが、プログラムを停止する代わりに、値を返し、プログラムを続行します。それは驚くほどうまく機能し、ボーナスとして、関数が「想定されていること」を返さないときにプログラムの残りの部分に何が起こるかを見ることができます。クラッシュした場合、エラーチェックがどこか他の場所で緩んでいることがわかります。

前回のプロジェクトでは、これらのスタイルの関数を使用して前提条件チェックを実装し、そのうちの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;
}
0
indiv