重複の可能性:
防御的プログラミングvs例外処理?
この質問がこのサイトまたはスタックオーバーフローに適していることはわかりませんが、私の質問は実践に関連しているため、特定の問題があります。
それで、何かをするオブジェクトを考えてください。そして、これは何かが起こる可能性があります(そうすべきではありません!)。したがって、この状況は2つの方法で解決できます。
まず、例外があります:
DoSomethingClass exampleObject = new DoSomethingClass();
try
{
exampleObject.DoSomething();
}
catch (ThisCanGoWrongException ex)
{
[...]
}
次に、ifステートメントを使用します。
DoSomethingClass exampleObject = new DoSomethingClass();
if(!exampleObject.DoSomething())
{
[...]
}
より洗練された方法での2番目のケース:
DoSomethingClass exampleObject = new DoSomethingClass();
ErrorHandler error = exampleObject.DoSomething();
if (error.HasError)
{
if(error.ErrorType == ErrorType.DivideByPotato)
{
[...]
}
}
どっちがいい?一方では、例外は実際の予期しない状況でのみ使用する必要があると聞きました。プログラマが何かが発生する可能性があることを知っている場合は、if/elseを使用する必要があります。一方、Robert C. Martinの著書 Clean Code は、例外ははるかにオブジェクト指向であり、クリーンに保つのがより簡単であると書いています。
通常、私は例外的なケースに例外を使用する傾向があります-何も問題が発生しないはずのインスタンスで、それが問題が発生した場合、それは大きな問題です。例としては、ソフトウェアに付属する構成ファイルの欠落や、ハードウェアドライバーの欠落などがあります。これらのことが起こった場合、それは大きな問題です。ユーザー入力やオプションのインスタンスなど、無効な情報が与えられる可能性が高い場合にチェックを実行します。
ただし、一部の言語は例外を支持するように見えることにも注意してください。たとえば、小さな問題でも例外を使用する方がPythonでははるかに一般的であることに気付きました。 Pythonコミュニティでは、これは EAFP-"許可より許しを求める方が簡単" として知られています。
エラー処理の通常の方法はこれです:
例外は例外的なケースを処理します。つまり、通常のプログラム実行の「ハッピーパス」に該当しないケースです。この非常に重要な警告を踏まえると、(a)何かが失敗する可能性があり、(b)実行時に何をすべきかがわかっている場合に、try/catchを使用することは一般に許容され、実際に予想されます。
Try-catchを使用しない主な状況は、メソッドの成功または失敗を少なくとも操作の実際の結果と同じくらい重要であると知る場合です。例は文字列の解析です。多くの場合、文字列が数字で構成されているかどうかを知ることは、実際の数字よりも少なくとも同じくらい役に立ちます。したがって、.NETは、プリミティブ型に組み込まれたParse関数の「TryParse」フレーバーを提供します。これらを使用すると、if/elseフォークの先頭で簡単な決定を行ってコードを構造化できます。これにより、一方または他方のコードブロックへのエントリが決まります。
たとえば、DoSomething()の成功が、コンソールに「はい」または「いいえ」のどちらを印刷するかを決定するとします。どちらもプログラムが行う「通常の」ことです。次のように書くことができます:
try
{
DoSomething();
Console.WriteLine("Yes");
}
catch(Exception)
{
Console.WriteLine("No");
}
...しかし、いくつかの問題があります。最初に、設計の観点から、DoSomething()が決定されていることがコメントなしでは明らかではありません。次に、DoSomething()は、例外をスローする可能性のあるtryブロックの唯一のものではない可能性があります。別の開発者が、DoSomethingが成功したときに実行する必要のある追加のコードを追加したとします。
try
{
DoSomething();
Console.WriteLine("Yes");
DoSomethingElse();
}
catch(Exception)
{
Console.WriteLine("No");
}
これで、DoSomething()が成功した後にDoSomethingElse()が失敗するシナリオがあり、1つの入力からエンドユーザーへの結果が非常に混乱します。
Yes
No
代わりに、これを考慮してください:
if(TryDoSomething()) //a "non-throwing" variant that returns success or failure
{
Console.WriteLine("Yes");
DoSomethingElse();
}
else
{
Console.WriteLine("No");
}
DoSomethingが2つの「通常の」実行パスの間の決定の基準であるという概念は、上記のコードではるかに明確です。そして、あなたが望むロジックフローは何があっても強制されます。 DoSomethingElse()が成功しても、DoSomethingElse()が何を実行しても、画面に「いいえ」と表示されることはありません。
他のほとんどの場合、以下の理由の1つ以上のため、try-catchが推奨されます。
私は彼らの情報伝達能力のために例外を使いました。 C++では、通常、関数の戻りコードは単一の値です。対照的に、必要な情報を含むクラスタイプの例外をスローできます。
例外を使用して、見つからなかったクエリの特定の部分を返しました。ほとんど使用されていないポインタまたは参照を7深さの呼び出しスタック内の各関数呼び出しに追加するよりも、それほど危険ではないように見えました。
if/else
実行が成功したかどうかに関係なく、毎回そのチェックを実行します。したがって、通常の状況でエラーがほとんどまたはほとんど発生しない場合は、例外処理がより効率的です。実行が成功した場合は、追加の条件を評価しないためです。
マルチスレッドまたは分散システムの場合のチェックに対する例外のもう1つの利点は、 TOCTTOU(チェックの時間から使用の時間)の問題 に起因する競合状態を回避することです。最初に条件をチェックしてから、条件が成立すると想定してコードを実行します。ロック/ミューテックスなどで原子性を保証することなく、コードの実行時に、条件は別のスレッド/プロセスによってすでに変更されている可能性があります。
考慮すべきもう1つのことは、例外が発生すると、その例外に関連するパフォーマンスの低下またはコストが発生することです。
たとえば、ループでX/Yを実行します。
1つの方法は、ifステートメントを使用することです。例:
if (Y != 0)
{
result = X/Y;
}
別の方法は、try/catchを使用することです。
try
{
result = X/Y;
}
catch (DevideByZeroException zeroEx)
{
//Log Error
}
これをループ(100万回)で実行すると、Y = 0の場合、tryステートメントよりもifステートメントの実行がはるかに速くなります。
だから、コーディングするときに自分自身に尋ねると、私はこの状態をたくさんヒットしますか?この例では、Yはどのくらいの頻度でゼロになりますか?時間の1%または50%? 50%の場合は、ifを使用します。たまに試してみてください。 500,000の例外が発生している場合、それは実際にはルールの例外ではなく、ルールです。あなたはそれを避けたいです。
ですから、例外はそんなに頻繁に起こらないようなものだと思う傾向があります。例外ロジックで処理される一般的なユースケースがある場合は、例外ではなくロジックステートメント(if)を使用するように変換する必要があります。ボーナスとして、コードもスピードアップします。
exampleObject.DoSomething();
が問題を引き起こさない場合は、例外を使用してください。
exampleObject.DoSomething()
がエラーを引き起こす有効で予期されるビジネスケースがある場合は、if/then/elseを使用し、コメントを使用して、これが問題を引き起こす可能性があるケースを説明します。
例:
Thing thing = findThingInDatabase();
//if a thing was found in the database (we EXPECT thing to not be found sometimes, hence if stmt as opposed to exception)
if (thing != null){
thing.doStuff();
}
X x;
try
{
x = exampleObject.DoSomething ();
}
catch (ThisCanGoWrongException ex)
DoSomethingが何かを返す場合でも、結果のスペースに十分なスペースがない限り、エラーを返すことはできません。この現象のよく知られている例は、1バイトを読み取ってintを返すCライブラリです。バイトは符号なしと見なされますが、エラーの場合は-1でした。
しかし今、あなたは醜いキャストを行う必要があります。
「無効」を意味する結果スペースに結果の余地がない場合はどうしますか?そうですね、T an Either [Error、T]の代わりに、返すラッパーを使用できます。通常、正しい結果は大丈夫な結果を意味し、左は一種のエラーを意味するために残されます。
特別な種類の値としてのエラーの代わりに、ライブラリのユーザーは-静的に型付けされた言語で-何か問題がある可能性があることを事前に通知されます。
対照的に、
String ips = getIP ("programmers.se.com");
// returns "err.or.in.DNS" in errorcase - but programmer
// forgot to check
for (String b : ips.split (".")) {
Integer.parseInt (b); // crash here
時々、メソッドは副作用のために呼び出され、戻り値はまったく検査されません。その後、エラーが検出されずにすり抜けることがあります。
user.setAge (-42);
たぶん、クラスは「false」または「無効な年齢です。0以上でなければなりません」というメッセージを返します。しかし、多くの言語では、返された結果を無視できます。ここでは例外が優先されます。
例外とラップされた結果の問題は、コードが乱雑になり、正常なケースを追跡することが難しくなることです。多分私たちは将来そこに改善を見るでしょう-Javaは例えば最近の例外の処理を単純化しようとしています。IDEはおそらくオンデマンドで例外コードを隠したり折り畳んだりするでしょう。
場合によっては、特別なNullオブジェクトを返すことが妥当な場合があります。データベースで特別なユーザーを検索することを検討してください。何かがうまくいかない場合、nullを返す可能性があります。
println (null.getName ()); // crash: NPE
よく見られる解決策は、通常の場合は1つの要素、欠陥の場合はゼロの値を持つリストを返すことです。
users = db.get ("Zappa");
for (User u : users) {
u.doSomething ();
これは1つまたはなしの要素で機能し、HaskellやScalaなどの言語には、MaybeまたはOptionという特別な目的のクラスがあります。
オプションはパラメーター化されたクラスです。 Option [T]は、一部(T)またはなしのいずれかです。ユーザーは結果を確認する必要がありますが、簡単に忘れられません。
これは空のリストの結果に似ており、どこでも適切であるとは限りませんが、多くの場合そうです。
例外は、データベースへの接続など、他のブロックが処理できなかった場合、データベースが使用できない場合にどうしますか?別のデータベースに接続しますか?または単に操作を中止しますか?そしてこれらのような他の状況はあなたが例外を使うべき状況です。
ただし、インデックスの範囲外のインデックスエラーが発生するなど、例外を使いすぎないようにしてください。その場合は、例外を使用します。