回答:
高レベルのものでは、例外。低レベルのものでは、エラーコード。
例外のデフォルトの動作は、スタックを巻き戻してプログラムを停止することです。スクリプトを作成していて、辞書にないキーを検索すると、エラーになる可能性があり、プログラムを停止して許可します。それについてすべて知っています。
ただし、考えられるすべての状況での動作を知っている必要があるコードを記述している場合は、エラーコードが必要です。それ以外の場合は、関数のすべての行によってスローされる可能性があるすべての例外を知って、それが何をするかを知る必要があります(これがどれほどトリッキーであるかを理解するには、航空会社を接地した例外を読んでください)。すべての状況(不幸な状況を含む)に適切に対応するコードを作成するのは面倒で困難ですが、それはエラーのないコードを作成するのが面倒で難しいためであり、エラーコードを渡すためではありません。
nodiscard
は、関数の戻り値が格納されていない場合にコンパイラ警告を表示する属性が導入されています。忘れられたエラーコードチェックをキャッチするのに少し役立ちます。例:godbolt.org/g/6i6E0B
私は通常、例外を優先します。例外はより多くのコンテキスト情報を持ち、(適切に使用すると)エラーをより明確にプログラマーに伝えることができるからです。
一方、エラーコードは例外よりも軽量ですが、保守が困難です。エラーチェックは不注意で省略される可能性があります。すべてのエラーコードを含むカタログを保持し、結果のスイッチを入れて、どのエラーがスローされたかを確認する必要があるため、エラーコードの管理は困難です。エラーの範囲は、ここで役立ちます。私たちが唯一関心があるのは、エラーが存在するかどうかである場合、チェックするのが簡単です(たとえば、0以上のHRESULTエラーコードが成功し、ゼロ未満は失敗です)。開発者がエラーコードをチェックするようにプログラムで強制する必要がないため、不注意でそれらを省略できます。一方、例外は無視できません。
要約すると、ほとんどすべての状況で、エラーコードよりも例外を優先します。
私は例外を好みます
goto
エラーコードの例外、それについて疑いの余地はありません。エラーコードの場合と同じように、例外から多くの利点を得ることができますが、エラーコードの欠点がありません。唯一の例外は、オーバーヘッドが少し増えることです。しかし、この時代では、そのオーバーヘッドはほとんどすべてのアプリケーションで無視できると考えられるべきです。
ここでは、2つの手法について説明、比較、対比している記事をいくつか示します。
あなたにさらに読むことができるそれらのいくつかの良いリンクがあります。
エラーコードを使用しているスタックの一部から、例外を使用しているより高い部分に移動するときに、2つのモデルを混合することは決してありません。
例外は、「メソッドまたはサブルーチンが実行するように要求したことを停止または禁止するもの」の場合です...異常または異常な状況、またはシステムの状態などについてメッセージを返しません。戻り値またはrefを使用します(またはout)そのためのパラメーター。
例外により、メソッドの機能に依存するセマンティクスでメソッドを記述(および利用)することができます。つまり、EmployeeオブジェクトまたはEmployeeのリストを返すメソッドを入力して、それを行うことができ、呼び出しによってそれを利用できます。
Employee EmpOfMonth = GetEmployeeOfTheMonth();
エラーコードを使用すると、すべてのメソッドがエラーコードを返すため、呼び出し元のコードで使用するために別のメソッドを返す必要がある場合は、そのデータを入力するための参照変数を渡して、すべての関数またはメソッド呼び出しでエラーコードを処理します。
Employee EmpOfMonth;
if (getEmployeeOfTheMonth(ref EmpOfMonth) == ERROR)
// code to Handle the error here
各メソッドが1つだけの単純なことを実行するようにコーディングする場合、メソッドがメソッドの目的を達成できない場合は常に例外をスローする必要があります。例外は、エラーコードよりもはるかに豊富で、この方法で簡単に使用できます。あなたのコードはもっときれいです-「通常の」コードパスの標準フローは、メソッドがあなたがやりたいことを達成できる場合にのみ当てることができます...そして、コードをクリーンアップまたは処理しますメソッドの正常な完了を妨げる問題が発生した「例外的な」状況は、通常のコードから分離できます。さらに、発生した場所で例外を処理できず、スタックをUIに渡す必要がある場合(さらには、ミッドティアコンポーネントからUIへのワイヤを介して)、例外モデルを使用して、
以前、私はエラーコードキャンプに参加しました(Cプログラミングが多すぎました)。しかし今、私は光を見てきました。
はい、例外はシステムに少し負担をかけます。しかし、それらはコードを単純化し、エラー(およびWTF)の数を減らします。
したがって、例外を使用しますが、賢く使用してください。そして、彼らはあなたの友達になります。
補足として。どのメソッドがどの例外をスローできるかを文書化することを学びました。残念ながら、これはほとんどの言語では必要ありません。ただし、適切な例外を適切なレベルで処理できる可能性が高くなります。
例外をクリーンで明確で正しい方法で使用するのが面倒な状況がいくつかあるかもしれませんが、ほとんどの場合、例外は明白な選択です。エラーコードに対する例外処理の最大の利点は、実行フローが変更されることです。これは、2つの理由で重要です。
例外が発生すると、アプリケーションは通常の実行パスをたどりません。これが非常に重要である最初の理由は、コードの作成者が上手く行き、本当に悪い方向に進んでいない限り、プログラムは停止し、予測できないことを続けないためです。エラーコードがチェックされず、不正なエラーコードに対応して適切なアクションが実行されない場合、プログラムは実行し続け、そのアクションの結果が誰にわかるかを判断します。プログラムに「何でも」実行させると、非常に高価になる可能性がある多くの状況があります。会社が販売するさまざまな金融商品のパフォーマンス情報を取得し、その情報をブローカー/卸売業者に配信するプログラムを考えてみましょう。何か問題が発生してプログラムが続行する場合、ブローカーや卸売業者に誤ったパフォーマンスデータを送る可能性があります。私は他の人については知りませんが、私のコードが会社に7桁に相当する規制上の罰金を科した理由を説明するVPのオフィスに座っている人にはなりたくありません。一般に、エラーメッセージを顧客に配信することは、「本物」に見える可能性のある間違ったデータを配信するよりも望ましい方法です。後者の状況は、エラーコードのようなそれほど積極的でないアプローチで実行するほうがはるかに簡単です。
私が例外と通常の実行の中断を好む2番目の理由は、「通常の問題が発生している」ロジックを「問題が発生したロジック」から分離することをはるかに簡単にすることです。私にとって、これ:
try {
// Normal things are happening logic
catch (// A problem) {
// Something went wrong logic
}
...これが望ましい:
// Some normal stuff logic
if (errorCode means error) {
// Some stuff went wrong logic
}
// Some normal stuff logic
if (errorCode means error) {
// Some stuff went wrong logic
}
// Some normal stuff logic
if (errorCode means error) {
// Some stuff went wrong logic
}
例外については、他にも良い点があります。関数で呼び出されているメソッドのいずれかにエラーコードが返されたかどうかを追跡するための一連の条件付きロジックがあり、そのエラーコードを上位に返すのは、多くのボイラープレートです。実際には、失敗する可能性のある多くのボイラープレートです。私は、ほとんどの言語の例外システムを、Fredが書いた「Fresh-out-of-college」というif-else-if-elseステートメントのネズミの巣を作るよりもずっと信頼しています。私の時間は、コードのレビューよりラットの巣をレビューしました。
両方を使用する必要があります。問題は、それぞれをいつ使用するかを決定することです。
例外が明らかな選択であるいくつかのシナリオがあります:
状況によっては、エラーコードで何もできず、コールスタックの上位レベルでそれを処理する必要があるだけです。通常は、エラーをログに記録するか、ユーザーに何かを表示するか、プログラムを閉じます。このような場合、エラーコードを使用すると、エラーコードを手動でレベルごとにバブルアップする必要があります。これは、例外を使用する方が明らかに簡単です。重要なのは、これは予期せぬ、処理不可能な状況であるということです。
ただし、状況1(予期せぬ処理不可能な事態が発生した場合、それをログに記録しないだけです)では、コンテキスト情報を追加する可能性があるため、例外が役立ちます。たとえば、下位レベルのデータヘルパーでSqlExceptionが発生した場合、そのエラーを低レベル(エラーの原因となったSQLコマンドがわかっている場所)でキャッチして、その情報を取得し、追加情報で再スローできるようにします。ここで魔法の言葉に注意してください:飲み込むのではなく、再スローする。 例外処理の最初のルール:例外を飲み込まないでください。また、外側のキャッチにはスタックトレース全体が含まれ、ログが記録される可能性があるため、内側のキャッチは何もログに記録する必要がないことに注意してください。
いくつかの状況ではコマンドのシーケンスがあり、それらのいずれかが失敗した場合は、リソース(*)をクリーンアップ/破棄する必要があります。これが回復不可能な状況(スローされる必要がある)であるか、回復可能な状況であるか(この場合、ローカルまたは呼び出し元コードで処理しますが、例外は必要ありません)。明らかに、各メソッドの後にエラーコードをテストして、finallyブロックでクリーンアップ/破棄するのではなく、これらすべてのコマンドを1回で試行する方がはるかに簡単です。ことをしてくださいノートあなただけのクリーンアップのために最終的に使用/処分-あなたは(あなたが望むおそらく)バブルアップにエラーをしたい場合は、あなたもそれをキャッチする必要はありません -あなたが望む場合にのみキャッチ/ retrowを使用する必要がありますコンテキスト情報を追加します(箇条書き2を参照)。
1つの例は、トランザクションブロック内の一連のSQLステートメントです。繰り返しますが、これも「処理できない」状況です。早期にキャッチすることを決定した場合でも(上部まで泡立つのではなくローカルで処理する)、すべてを中止するか、少なくとも大規模なバージョンを中止することが最良の結果となる致命的な状況です。プロセスの一部。
(*)これはon error goto
、以前のVisual Basicで使用していたものと似ています
コンストラクターでは、例外のみをスローできます。
そうは言っても、呼び出し元が何らかのアクションを実行できるかどうかに関する情報を返す他のすべての状況では、戻りコードを使用する方がおそらくより良い代替手段です。これには、予想されるすべての「エラー」が含まれます。これは、おそらく直接の呼び出し側で処理する必要があり、スタック内の多くのレベルをバブルアップする必要がないためです。
もちろん、予期されるエラーを例外として扱い、すぐ上のレベルでキャッチすることは常に可能です。また、コードのすべての行をtryキャッチに含め、考えられるエラーごとにアクションを実行することも可能です。IMO、これは悪いデザインです。これは、はるかに冗長であるためだけでなく、特に、スローされる可能性のある例外がソースコードを読み取らなければ明らかではないためです。また、例外は、深いメソッドからスローされ、見えないゴトスを作成する可能性があります。それらは、コードの読み取りと検査を困難にする複数の非表示の出口点を作成することにより、コード構造を壊します。つまり、フロー制御として例外を使用してはいけません。、それは他の人が理解して維持するのが難しいからです。テストのために考えられるすべてのコードフローを理解することはさらに難しくなる可能性があります。
繰り返しますが、正しいクリーンアップ/破棄のために、何もキャッチせずにtry-finallyを使用できます。
リターンコードに関する最も一般的な批判は、「誰かがエラーコードを無視することができますが、同じ意味で誰かが例外を飲み込むこともできます。悪い例外処理はどちらの方法でも簡単です。しかし、優れたエラーコードベースのプログラムを書くことは、はるかに簡単です。例外ベースのプログラムを作成するよりも。何らかの理由ですべてのエラー(古いon error resume next
)を無視することにした場合は、戻りコードを使用して簡単にそれを行うことができ、多数のtry-catchsボイラープレートなしでそれを行うことはできません。
リターンコードに関する2番目に人気のある批判は、「バブルアップするのは難しい」ということです。それは、エラーコードがそうではないのに、例外が回復不可能な状況にあることを人々が理解していないためです。
例外とエラーコードを区別するのは、灰色の領域です。いくつかの再利用可能なビジネスメソッドからエラーコードを取得する必要があり、それを例外にラップして(おそらく情報を追加する)、それをバブルアップさせることさえ可能です。ただし、すべてのエラーを例外としてスローする必要があると想定するのは、設計ミスです。
要約すると:
予期せぬ状況が発生したときに例外を使用するのが好きです。この場合、実行することはほとんどなく、通常、大きなコードブロックまたは操作全体またはプログラム全体を中止する必要があります。これは古い「エラー時のgoto」に似ています。
呼び出し元のコードが何らかのアクションを実行できる/すべきであると予想される状況で、リターンコードを使用したい。これには、ほとんどのビジネスメソッド、API、検証などが含まれます。
例外とエラーコードのこの違いは、GO言語の設計原則の1つであり、致命的な予期しない状況に対して「パニック」を使用し、通常の予期される状況はエラーとして返されます。
ただし、GOについては、複数の戻り値も許可されます。これは、エラーと他の何かを同時に返すことができるため、戻りコードの使用に非常に役立ちます。C#/ Javaでは、列挙型と組み合わせたoutパラメーター、タプル、または(私のお気に入りの)ジェネリックを使用して、呼び出し元に明確なエラーコードを提供できます。
public MethodResult<CreateOrderResultCodeEnum, Order> CreateOrder(CreateOrderOptions options)
{
....
return MethodResult<CreateOrderResultCodeEnum>.CreateError(CreateOrderResultCodeEnum.NO_DELIVERY_AVAILABLE, "There is no delivery service in your area");
...
return MethodResult<CreateOrderResultCodeEnum>.CreateSuccess(CreateOrderResultCodeEnum.SUCCESS, order);
}
var result = CreateOrder(options);
if (result.ResultCode == CreateOrderResultCodeEnum.OUT_OF_STOCK)
// do something
else if (result.ResultCode == CreateOrderResultCodeEnum.SUCCESS)
order = result.Entity; // etc...
メソッドに新しい可能な戻り値を追加すると、たとえば、switchステートメントでその新しい値をカバーしているかどうか、すべての呼び出し元をチェックすることもできます。あなたは本当にそれを例外なく行うことはできません。通常、戻りコードを使用すると、起こり得るすべてのエラーを事前に把握し、それらをテストできます。例外を除いて、あなたは通常何が起こるかわからない。(ジェネリックスの代わりに)例外内で列挙型をラップすることは(各メソッドがスローする例外のタイプが明らかである限り)代替手段ですが、IMOは依然として悪い設計です。
私はここのフェンスに座っているかもしれませんが...
Pythonでは、例外の使用が標準的な方法であり、独自の例外を定義できてとてもうれしいです。Cでは、例外はまったくありません。
C ++では(少なくともSTLでは)、例外は通常、本当に例外的なエラーに対してのみスローされます(私は実際に自分でそれらを目にすることはありません)。私自身のコードで何か違うことをする理由はないと思います。はい、戻り値を無視するのは簡単ですが、C ++は例外をキャッチするように強制しません。私はあなたがそれをする習慣に入る必要があると思います。
私が取り組んでいるコードベースは主にC ++であり、ほとんどどこでもエラーコードを使用しますが、非常に例外的なものを含め、すべてのエラーに対して例外を発生させるモジュールが1つあり、そのモジュールを使用するすべてのコードはかなり恐ろしいものです。しかし、それは、例外とエラーコードが混在しているためと考えられます。エラーコードを一貫して使用するコードは、操作がはるかに簡単です。私たちのコードが一貫して例外を使用した場合、多分それほど悪くはないでしょう。2つを混ぜてもうまくいかないようです。
私のアプローチは、例外コードとエラーコードの両方を同時に使用できることです。
私はいくつかのタイプの例外(例:DataValidationExceptionまたはProcessInterruptExcepion)を定義するために使用され、各例外内で各問題のより詳細な説明を定義します。
Javaの簡単な例:
public class DataValidationException extends Exception {
private DataValidation error;
/**
*
*/
DataValidationException(DataValidation dataValidation) {
super();
this.error = dataValidation;
}
}
enum DataValidation{
TOO_SMALL(1,"The input is too small"),
TOO_LARGE(2,"The input is too large");
private DataValidation(int code, String input) {
this.input = input;
this.code = code;
}
private String input;
private int code;
}
このように、例外を使用してカテゴリエラーを定義し、エラーコードを使用して、問題に関するより詳細な情報を定義します。
throw new DataValidationException("The input is too small")
?例外の利点の1つは、詳細情報を許可できることです。
例外は例外的な状況、つまりコードの通常のフローの一部ではない場合です。
例外とエラーコードを混在させることは非常に正当です。エラーコードは、コードの実行自体のエラー(子プロセスからの戻りコードのチェックなど)ではなく、何かのステータスを表します。
しかし、例外的な状況が発生した場合、例外は最も表現力のあるモデルだと思います。
例外の代わりにエラーコードを使用したり、使用したりするケースがあり、これらはすでに適切にカバーされています(コンパイラサポートなどの他の明らかな制約以外)。
しかし、反対の方向に進むと、例外を使用すると、エラー処理にさらに高いレベルの抽象化を構築でき、コードをさらに表現力豊かで自然なものにすることができます。:私は非常に彼は、「強制の」と呼ぶものの対象にC ++の専門家アンドレイアレキすることにより、このまだ過小評価に優れ、記事を読んで推薦するhttp://www.ddj.com/cpp/184403864。これはC ++の記事ですが、原則は一般的に適用可能であり、私は施行の概念をC#にかなりうまく翻訳しました。
まず、サービス指向アーキテクチャー(SOA)でない限り、高レベルのものには例外を使用し、低レベルのものにはエラーコードを使用するというトムの回答に同意します。
メソッドが異なるマシン間で呼び出される可能性があるSOAでは、例外はネットワーク経由で渡されない場合があります。代わりに、以下のような構造を持つ成功/失敗応答を使用します(C#):
public class ServiceResponse
{
public bool IsSuccess => string.IsNullOrEmpty(this.ErrorMessage);
public string ErrorMessage { get; set; }
}
public class ServiceResponse<TResult> : ServiceResponse
{
public TResult Result { get; set; }
}
そして、このように使用します:
public async Task<ServiceResponse<string>> GetUserName(Guid userId)
{
var response = await this.GetUser(userId);
if (!response.IsSuccess) return new ServiceResponse<string>
{
ErrorMessage = $"Failed to get user."
};
return new ServiceResponse<string>
{
Result = user.Name
};
}
これらをサービス応答で一貫して使用すると、アプリケーションで成功/失敗を処理する非常に優れたパターンが作成されます。これにより、サービス内だけでなくサービス内の非同期呼び出しでのエラー処理が容易になります。
失敗がプリミティブデータ型を返す関数の期待できるバグのない結果である場合を除いて、すべてのエラーの場合は例外を優先します。たとえば、大きな文字列内の部分文字列のインデックスを検索すると、NotFoundExceptionが発生するのではなく、通常、見つからない場合は-1が返されます。
逆参照される可能性のある無効なポインタを返すこと(たとえば、JavaでNullPointerExceptionを引き起こす)は受け入れられません。
複数の異なる数値エラーコード(-1、-2)を同じ関数の戻り値として使用すると、クライアントが "<= 0"ではなく "== -1"チェックを実行する可能性があるため、通常は不適切なスタイルです。
ここで覚えておくべきことの1つは、時間の経過に伴うAPIの進化です。優れたAPIを使用すると、クライアントを壊すことなく、いくつかの方法で障害の動作を変更および拡張できます。たとえば、クライアントエラーハンドルが4つのエラーケースをチェックし、5番目のエラー値を関数に追加した場合、クライアントハンドラーはこれをテストせずに中断する可能性があります。例外を発生させると、通常、これによりクライアントがライブラリの新しいバージョンに移行しやすくなります。
考慮すべきもう1つのことは、チームで作業するときに、すべての開発者がそのような決定を行うための明確な線を引く場所です。例えば、「高レベルのものの例外、低レベルのもののエラーコード」は非常に主観的です。
いずれにせよ、2つ以上の些細なタイプのエラーが発生する可能性がある場合、ソースコードは数値リテラルを使用してエラーコードを返したり処理したりすることはできません(x == -7の場合は-7を返します)。常に名前付き定数(x == NO_SUCH_FOOの場合はNO_SUCH_FOOを返します)。
結果からスタックトレースなどの情報が本当に必要かどうかにも依存すると思います。はいの場合、問題に関する多くの情報が満載のオブジェクトを提供する例外に間違いなく行きます。ただし、結果だけに関心があり、その結果がなぜかを気にしない場合は、エラーコードを取得してください。
たとえば、ファイルを処理してIOExceptionに直面している場合、クライアントは、これがトリガーされた場所から、ファイルを開いたり、ファイルを解析したりすることなどに関心を持っている可能性があります。ただし、ログインメソッドがあり、それが成功したかどうかを知りたい場合は、ブール値を返すか、正しいメッセージを表示してエラーコードを返します。ここで、クライアントはロジックのどの部分がそのエラーコードを引き起こしたかを知ることに興味がありません。彼は、その資格情報が無効であるか、アカウントがロックされているかなどを知っています。
私が考えることができるもう1つのユースケースは、データがネットワーク上を移動するときです。リモートメソッドは、データ転送を最小限に抑えるために、例外ではなくエラーコードのみを返すことができます。
メソッドが数値以外を返す場合も、エラーコードは機能しません...