プログラミングの古典的な方法は、try ... catch
。 try
なしでcatch
を使用するのが適切なのはいつですか?
Pythonでは、以下は合法であると思われ、理にかなっています:
try:
#do work
finally:
#do something unconditional
ただし、コードは何もcatch
しませんでした。同様に、Javaと考えると、次のようになります。
try {
//for example try to get a database connection
}
finally {
//closeConnection(connection)
}
見栄えが良く、突然例外の種類などを気にする必要がなくなります。これが良い方法である場合、いつ良い方法ですか?あるいは、これが良い習慣や合法ではない理由は何ですか? (私はソースをコンパイルしませんでした。Javaの構文エラーの可能性があるので、それについて尋ねています。Pythonが確実にコンパイルされることを確認しました。)
私が遭遇した関連する問題はこれです:私は関数/メソッドを書き続けますが、最後に何かを返さなければなりません。ただし、到達すべきではない場所にある可能性があり、戻りポイントでなければなりません。したがって、上記の例外を処理した場合でも、到達できないコードのある時点でNULL
または空の文字列を返します。多くの場合、メソッド/関数の終わりです。私は常にコードを再構築して、return NULL
、それは絶対に良い習慣とは思えないからです。
この時点で発生する可能性のある例外に対処できるかどうかによって異なります。
例外をローカルで処理できる場合は、そうする必要があります。発生した場所にできるだけ近い場所でエラーを処理することをお勧めします。
ローカルで処理できない場合は、try / finally
ブロックを使用するだけで十分です。メソッドが成功したかどうかに関係なく、実行する必要があるコードがあると仮定します。たとえば(from Neil's comment )、ストリームを開いてからロードする内部メソッドにそのストリームを渡すのは、finally句を使用してtry { } finally { }
が必要な場合の優れた例です読み取りの成功または失敗に関係なく、ストリームが確実に閉じられるようにします。
ただし、アプリケーションを完全にクラッシュさせたくない場合を除いて、コード内のどこかに例外ハンドラが必要です。これは、ハンドラーがどこにあるか、アプリケーションのアーキテクチャーに依存します。
finally
ブロックは、エラー条件(例外)が発生したかどうかにかかわらず、常に実行する必要があるコードに使用されます。
finally
ブロックのコードは、try
ブロックが完了した後に実行され、キャッチされた例外が発生した場合は、対応するcatch
ブロックが完了した後に実行されます。 try
またはcatch
ブロックでキャッチされない例外が発生した場合でも、常に実行されます。
finally
ブロックは通常、try
ブロックで開かれたファイルやネットワーク接続などを閉じるために使用されます。その理由は、ファイルまたはネットワーク接続を使用する操作が成功したか失敗したかに関係なく、ファイルまたはネットワーク接続を閉じる必要があるためです。
finally
ブロック自体で例外がスローされないように注意する必要があります。たとえば、null
などのすべての変数を必ず確認してください。
try ...最後にcatch句なしが適切な例(さらに、idiomatic )in Javaは、同時ユーティリティロックパッケージでのLockの使用法です。
...ブロック構造のロックがないため、同期されたメソッドとステートメントで発生するロックの自動解放が削除されます。ほとんどの場合、次のイディオムを使用する必要があります:
Lock l = ...; l.lock(); try { // access the resource protected by this lock } finally { l.unlock(); }
ロックとロック解除が異なるスコープで発生する場合、ロックが保持されている間に実行されるすべてのコードが、try-finallyまたはtry-catchによって確実に保護され、確実にロックされるようにする必要があります。必要に応じて解放されます。
基本的なレベルでは、catch
とfinally
は、2つの関連するが異なる問題を解決します。
catch
は、呼び出したコードによって報告された問題を処理するために使用されますfinally
は、問題が発生したかどうかに関係なく、現在のコードが作成/変更したデータ/リソースをクリーンアップするために使用されますしたがって、bothは何らかの形で問題(例外)に関連していますが、それは彼らが共通して持っているほとんどすべてです。
重要な違いは、finally
ブロックmustは、リソースが作成されたのと同じメソッド内にあり(リソースリークを回避するため)、呼び出しで別のレベルに置くことができないことです。スタック。
ただし、catch
は別の問題です。正しい場所は、例外を実際に処理できる場所によって異なります。何もできない場所で例外をキャッチしても意味がないため、単純に通過させた方がよい場合があります。
@yfeldblumの正解は次のとおりです。try-finallyにcatchステートメントがない場合は、通常、適切な言語構成に置き換える必要があります。
C++では、RAIIとコンストラクター/デストラクターを使用しています。 Pythonはwith
ステートメントです; C#ではusing
ステートメントです。
初期化と終了のコードが2つの場所ではなく1つの場所(抽象化されたオブジェクト)にあるため、これらはほとんど常にエレガントです。
多くの言語では、finally
ステートメントもreturnステートメントの後に実行されます。つまり、次のようなことができます。
try {
// Do processing
return result;
} finally {
// Release resources
}
これは、メソッドが例外または通常のreturnステートメントで終了した方法に関係なく、リソースを解放します。
これが良いか悪いかは議論の余地がありますが、try {} finally {}
は常に例外処理に限定されるわけではありません。
私はPythonistasの怒りを呼ぶかもしれません(私はPython much)または他の言語のプログラマをこの答えで使用していないのでわかりません)が、私の意見ではほとんどの関数には、理想的に言えば、catch
ブロックがない必要があります。理由を示すために、これを手動のエラーコードの種類の伝播と比較してみましょう80年代後半から90年代前半にTurbo Cを使用するときに私はしなければなりませんでした。
したがって、ユーザーがロードする画像ファイルを選択したことに応答して、画像またはそのようなものをロードする関数があるとします。これは、Cおよびアセンブリで記述されています。
低レベルの関数をいくつか省略しましたが、エラー処理に関してそれらが持つ責任に基づいて、色分けされた関数のさまざまなカテゴリを識別したことがわかります。
障害点と回復のポイント
今、私が「障害の可能性のあるポイント」(throw
、つまり)と呼ぶ関数のカテゴリと、「エラー回復とレポート」関数(catch
、つまり)。
これらの関数は、メモリの割り当てに失敗するなどの外部障害に遭遇する可能性のある関数はNULL
または_0
_または_-1
_または、グローバルエラーコードまたは何かをこの効果に設定します。また、コールスタックをたどり、障害を回復して報告するのが理にかなっているポイントまでたどり着くと、エラーコードやメッセージを取得してユーザーに報告するだけなので、エラーの回復/報告は常に簡単です。そして当然、この階層のリーフにある関数は、将来どのように変更されても決して失敗することはありません(_Convert Pixel
_)。
エラーの伝播
ただし、ヒューマンエラーが発生しやすい退屈な関数は、エラープロパゲーターです。これは、直接障害が発生することはなく、階層のより深いところで障害が発生する可能性がある関数を呼び出しました。その時点で、_Allocate Scanline
_はmalloc
からの失敗を処理し、_Convert Scanlines
_までエラーを返す必要がある場合があります。その後、_Convert Scanlines
_はそのエラーを確認して渡す必要があります_Decompress Image
_、次に_Decompress Image->Parse Image
_、および_Parse Image->Load Image
_、および_Load Image
_まで下げて、エラーが最終的に報告されるユーザーエンドコマンドに渡します。
エラーの適切な処理に関しては、関数の階層全体が機能しなくなってしまうため、エラープロパゲーターが1つだけチェックしてエラーを渡さないため、多くの人間がミスを犯します。
さらに、エラーコードが関数によって返された場合、たとえば、コードベースの90%では、successで目的の値を返す機能がかなり失われます。失敗時にエラーコードを返すために戻り値を予約する必要があります。
人的エラーの削減:グローバルエラーコード
では、人的ミスの可能性をどのようにして減らすことができるでしょうか?ここで私は一部のCプログラマーの怒りを呼ぶことさえあるかもしれませんが、私の意見の即時の改善は、glGetError
を伴うOpenGLのようなglobalエラーコードを使用することです。これにより、少なくとも関数は解放され、成功時に重要な意味のある値が返されます。エラーコードがスレッドにローカライズされる場合、このスレッドを安全かつ効率的にする方法があります。
関数がエラーに遭遇する場合もありますが、以前のエラーを発見した結果、途中で復帰する前に少し長く続けることは、比較的無害です。これにより、すべての単一関数で行われた関数呼び出しの90%に対してエラーをチェックする必要がなく、そのようなことが発生するため、細心の注意を払わなくても適切なエラー処理を行うことができます。
人的エラーの削減:例外処理
ただし、上記のソリューションでは、手動の_if error happened, return error
_タイプのコードの行数を減らした場合でも、手動エラー伝播の制御フローの側面を処理するために、非常に多くの関数が必要です。少なくとも1か所でエラーをチェックし、ほぼすべてのエラー伝播関数を返す必要があることが多いため、完全に排除されるわけではありません。だから、これは例外処理がその日を救うために登場するときです(ちょっと)。
ただし、ここでの例外処理の価値は、手動エラー伝搬の制御フローの側面を処理する必要をなくすことです。つまり、その値は、コードベース全体でcatch
ブロックのボートロードを書き込む必要を回避する機能に関連付けられています。上の図で、catch
ブロックが必要なのは、エラーが報告される_Load Image User Command
_だけです。それ以外の場合、理想的には何もcatch
にする必要はありません。そうしないと、エラーコードの処理と同じくらい退屈でエラーが発生しやすくなります。
だから私に尋ねると、エレガントな方法で例外処理から本当に利益を得るコードベースがある場合、それはminimum数のcatch
ブロック(最低でも私はゼロを意味するのではなく、失敗する可能性のあるすべてのユニークなハイエンドユーザー操作に対して1のようになり、すべてのハイエンドユーザー操作が中央のコマンドシステムを介して呼び出される場合はさらに少なくなります)。
リソースのクリーンアップ
ただし、例外処理は、通常の実行フローとは別の例外パスでのエラー伝播の制御フローの側面を手動で処理することを回避する必要性のみを解決します。多くの場合、エラー伝達関数として機能する関数は、EHを使用してこれを自動的に実行しても、破棄する必要のあるリソースを取得する可能性があります。たとえば、このような関数は、関数から戻る前に閉じる必要のある一時ファイルを開いたり、ロック解除するのに必要なミューテックスをロックしたりします。
このため、あらゆる種類の言語から多くのプログラマの怒りを呼ぶかもしれませんが、これに対するC++のアプローチは理想的だと思います。言語はdestructorsを導入します。これは、オブジェクトがスコープから外れると決定論的な方法で呼び出されます。このため、たとえば、デストラクターを使用してスコープ付きmutexオブジェクトを介してmutexをロックするC++コードは、手動でロックを解除する必要はありません。例外が発生しても、オブジェクトがスコープから外れると自動的にロック解除されるためです。遭遇した)。そのため、ローカルリソースのクリーンアップを処理するために、適切に作成されたC++コードを使用する必要はありません。
デストラクタが不足している言語では、finally
ブロックを使用してローカルリソースを手動でクリーンアップする必要がある場合があります。とは言っても、手動でエラーを伝播させるとコードを散らかす必要があるとはいえ、すさまじい場所全体で例外をcatch
する必要はありません。
外的な副作用の逆転
これはthe解決するのが最も難しい概念的な問題です。エラー伝達関数または障害ポイントのいずれかである関数が外部の副作用を引き起こす場合、それらの副作用をロールバックまたは「元に戻し」、システムが「操作が行われなかった」状態ではなく、操作が発生しなかったかのような状態に戻す必要があります。操作が途中で成功した「半有効」状態。不変性と永続的なデータ構造を中心に機能する関数型言語のように、ほとんどの関数が最初から外部の副作用を引き起こす必要性を単に減らす言語を除いて、この概念的な問題をはるかに容易にする言語はないことを知っています。
ここでfinally
はおそらく、可変性と副作用を中心に展開する言語の問題に対する最もエレガントな解決策の1つです。このタイプのロジックは特定の関数に非常に固有であり、それほど適切にマッピングされないためです「リソースのクリーンアップ」の概念。また、finally
ブロックが必要かどうかにかかわらず、関数がそれをサポートする言語の副作用を確実に元に戻すために、catch
を自由に使用することをお勧めします(また、私、適切に記述されたコードには、最小数のcatch
ブロックが必要です。すべてのcatch
ブロックは、上記の_Load Image User Command
_の図のように、最も意味のある場所にある必要があります)。
夢の言語
ただし、IMO finally
は、副作用の逆転には理想的ですが、完全ではありません。次のように、(スローされた例外などから)途中で終了した場合の副作用を効果的にロールバックするには、1つのboolean
変数を導入する必要があります。
_bool finished = false;
try
{
// Cause external side effects.
...
// Indicate that all the external side effects were
// made successfully.
finished = true;
}
finally
{
// If the function prematurely exited before finishing
// causing all of its side effects, whether as a result of
// an early 'return' statement or an exception, undo the
// side effects.
if (!finished)
{
// Undo side effects.
...
}
}
_
私が言語を設計することができれば、この問題を解決する私の夢の方法は、上記のコードを自動化するために次のようになるでしょう:
_transaction
{
// Cause external side effects.
...
}
rollback
{
// This block is only executed if the above 'transaction'
// block didn't reach its end, either as a result of a premature
// 'return' or an exception.
// Undo side effects.
...
}
_
...ローカルリソースのクリーンアップを自動化するデストラクタを使用して、transaction
、rollback
、およびcatch
のみが必要になるようにします(ただし、finally
は、たとえば、自分自身をクリーンアップしないCリソースを操作するためです)。ただし、finally
変数を使用したboolean
は、これを簡単にするのに最も近い方法であり、これまでのところ、夢の言語が不足していることがわかりました。これについて私が見つけた2番目に簡単な解決策は、C++やDなどの言語のscope Guardsですが、スコープガードは、概念的にぼやけているため、概念的には少し厄介です。 「リソースのクリーンアップ」と「副作用の逆転」。私の意見では、これらは異なる方法で取り組むべき非常に異なるアイデアです。
言語についての私の小さな夢はまた、不変性と永続的なデータ構造を中心に展開し、必須ではありませんが、大量のデータ構造全体をディープコピーする必要がない効率的な関数を簡単に作成できるようにします。副作用はありません。
結論
とにかく、私のとりとめを別にして、ソケットを閉じるための_try/finally
_コードは、PythonにC++のデストラクタに相当するものがないため、個人的には副作用を逆転させ、最も意味のある場所にcatch
する必要がある場所の数を最小限に抑える必要がある場所では、これを自由に使用する必要があると考えます。