ようこそ。睡眠不足なプログラマのチラ裏です。

いまさら聞けない.NET テクノロジの例外管理の設計および実装のガイドライン その2

例外の検出

.NET Framework では、他の一般的な水準のオブジェクト指向プログラミング言語と同じように、
構造化例外処理を利用して例外を適切に処理することができる。


構造化例外処理とは、例外と保護されたコードブロック、およびフィルタを含む制御構造によって、
例外処理を堅牢かつ効率的に行うことができる仕組みである。
try、catch、および finally ブロックを使い、コード内で送出された例外を検出し、
ログを記録したり復旧を試みるなど、適切な対応をとることができる。


まず、例外を送出する可能性のあるコードがtry ブロックに記述される。tryブロック内で例外が送出されると、
その例外のクラスにマッチするフィルタを持つ最初の catch ブロックが例外をキャッチする。
複数の catch ブロックを置く場合には、具体的な型から一般的な型の順に並べる必要がある。
そうしなければ、最も具体的な型の catch ブロックが実行されることが保証がされなくなってしまうからだ。
finally ステートメントでは、例外が送出されたかどうかにかかわらずtryブロック実行後、必ず実行される。
ここでは、リソースの開放など、必ず実行されなくてはならないコードを記述する。

//[C#]
try
{
  // 例外を送出する可能性のある何らかのコード
}
catch(SomeException someex)
{
  //最も優先となる例外キャッチフィルタ
  // InnerExceptionを付加するなど
  // 例外の発生に 対処する何らかのコード
}
catch(Exception ex){
  // SomeExceptionの次の例外キャッチフィルタ
}
finally
{
  // 例外が送出されたかどうかにかかわらず、常に実行されるコード
  // いわゆる、クリーンアップ コード
}
'[VB.NET]
Try
  ' 例外を送出する可能性のある何らかのコード
Catch someex As SomeException
  ' 最も優先となる例外キャッチフィルタ
  ' InnerExceptionを付加するなど
  ' 例外の発生に 対処する何らかのコード
Catch ex as Exception
  'SomeExceptionの次の例外キャッチフィルタ
Finally
  ' 例外が送出されたかどうかにかかわらず、常に実行されるコード
  ' いわゆる、クリーンアップ コード
End Try


例外は適切に使用する

コードが例外を送出する条件は、コードが仮定している条件に外れた事象が発生したときにのみである。
言い換えれば、意図した機能を提供する手段として、例外の送出を行うことは、例外の誤った利用方法である。


たとえば、アプリケーションにログオンするときにユーザーが無効なユーザー名やパスワードを入力することがある。
この場合、ログオンは失敗するが、これは予期された有効な結果であり、例外を送出すべきではない。
一方、ログオン時にアクセスするユーザー データベースへの接続が不可能であるというような
予期しない条件が発生したようなときには、例外を送出すべきである。
例外の送出は、単に呼び出し元に結果を返すよりもコストがかかるので、
コード内の通常の実行の流れを制御するという目的で、例外を使用するのは原則としてしてはならない。
また、例外を多用すると、コードの可読性が失われ、管理の難しいコードになってしまう可能性があるので注意が必要である。



CLRインターセプトする例外

シナリオによっては、送出した例外がランタイムによってキャッチされ、
別の型の例外が元の例外の代わりに呼び出しスタック上に送出されることがある。


たとえば、オブジェクトの ArrayList の Sort メソッドを呼び出す場合、
いずれかのオブジェクトが IComparable インターフェイスの CompareTo メソッドの中で例外を送出する場合、
その例外はランタイムによってキャッチされ、Sort メソッドを呼び出したコードに対して
System.InvalidOperationException 例外が送出される。
さらに、リフレクションを通して呼び出したメソッドによって送出されたすべての例外は、ランタイムによってキャッチされ、
System.Reflection.TargetInvocationException が送出される。
これらのシナリオでは、元の例外は失われず、発生元例外をInnerExceptionとしてプロパティに保持される。


例外を伝播させる方法

例外を伝播させる方法は、大きく分けて3つがある。

1.例外を自動的に伝播させる

プログラマは何も行わず、例外を意図的に無視し、上位レイヤへ伝播させる
この場合、制御は例外型にマッチするフィルタを持つ catch ブロックが見つかるまで、
現在のコード ブロックから呼び出しスタックを遡上する。

2.例外をキャッチ(捕捉)し、例外処理を行ってから伝播させる

このアプローチは、例外を捕捉し、ログ出力やリソースの解放などクリーンアップを行った後、
例外から復旧できない場合、同じ例外を上位のレイヤへ再スローする。

3.例外をキャッチし、ラッピングしてから伝播させる

例外は伝播するにつれ、例外型の具体性が徐々に低下していくため、
より具体的な例外にラッピングして(InnerException)呼び出し元の上位レイヤに再スローする。
例外をラッピングすると、より具体的な例外を呼び出し元に返すことができる。


このうち、3.の例外のラッピングを行う場合は注意が必要となる。
というのも、最初にスローされた例外には例外発生の原因に関する具体的な情報が格納されており、
運用時または開発時における問題の特定に役立つことが多い。
故に、捕捉した例外を別の例外としてラッピングする場合は、再スローする別の例外のコンストラクタ
捕捉した例外をパラメータとして渡し、最初に捕捉した例外を明示的にInnerExceptionプロパティに保存しておく必要がある。

また、例外が呼び出しスタック上を伝播するときは、catch ブロックは外側例外のみをキャッチするので、
InnerExceptionは、コード上でプロパティを通してアクセスすることができるが、
そのままでは、InnerExceptionは、catch ブロックとのマッチングは行われない点も注意する必要がある。


ThreadExceptionEventHandler デリゲートとUnhandledExceptionEventHandler デリゲート
Windows Forms アプリケーションでは、捕捉されなかった例外がスローされると
Application.ThreadExceptionイベントが発生する。このThreadExceptionイベントをハンドルして処理することにより、
メインスレッドで発生した未処理の例外を一元管理することができる。


また、メインスレッド以外のコンテキスト上で発生した例外は、
現在のアプリケーションドメイン(=AppDomainクラス(System名前空間)のオブジェクトにより表され、
AppDomain.UnhandledExceptionイベントをハンドルして処理することができる。


.NET Frameworkでは、上記2つのイベントハンドラを利用することで、Windows Formsアプリケーションの例外を
効率的かつ適切に処理することができる。ただし、非同期デリゲート内で例外が発生した場合、
UnhandledException のイベントハンドラを設定しても、適切に例外をキャッチすることができない。
非同期デリゲート内で発生した例外をキャッチするには、
非同期デリゲートの処理時に呼び出されるコールバックメソッドのなかで、例外をキャッチし、
BiginInvokeメソッドを使い、非同期デリゲートを呼び出した側のUIスレッドのメソッドを呼び出し、
そのUIスレッドのメソッドの方で、例外を再スローさせることで、例外をキャッチすることができる。
(ただし、UIスレッドとは別のスレッドのInnerExceptionは消失してしまう。)