例外処理。Try Catch、Try Finally、既定例外の基本的な動きと特性の理解。(UnhandledException,ThreadException)

例外処理の基本的な動きと特性について整理してみました。まずは、基本から。

  • Try Catch

Tryブロックの範囲で実行されるコードは例外が発生した場合に必ずCatchブロックの処理が呼び出される事が保証されます。

  • Try Finally

Tryブロックの範囲で実行されるコードでの例外の発生の有無に関係なく、必ずFinallyブロックの処理が呼び出される事が保証されます。



Finally は例外処理というより終了処理として考えてください。実際、例外の処理を行うわけではありません。Try-Finallyブロック内で発生した例外は外側のTryブロックで例外が処理されます。



さて、外側のブロックにもTry Catchが無い場合はどうなるでしょう。.Netでは何も設定していない場合は以下の画面が表示されます。

ユーザが終了を選んだ場合は、アプリケーションは終了します。(WindowsFormなどのUIを持つスレッドの場合)


また、プログラム内部で作成したスレッドの中で例外が発生した場合は以下の画面になります。

この場合は終了しか選べないようです。(UIを持たないスレッドの場合)




★★★★★★★★★★★★★



例外を適切に処理するには、すべての処理をTry Catchで囲まなければいけないのでしょうか?そうではありません。既定の例外処理を設定することができます。既定の例外処理を設定するとTry Catchを設定していないブロックで発生した例外は、あらかじめ設定した例外処理で処理されるようになります。


既定例外処理は、WindowsFormなどの画面をもつUIスレッドと、UIを持たないThreading.Thread.Startなどで実行するスレッドで分けて設定する必要があります。

  • UIスレッドの場合

アプリケーションドメインのUnhandledExceptionイベントハンドラと、アプリケーションのThreadExceptionハンドラにエラー処理ルーチンを設定することで既定例外を実装できます。スレッドレベルで既定例外を設定しているのでアプリケーションドメインレベルまで例外が通知されることはありませんが念のために設定しておきましょう。



Thread.GetDomain.UnhandledException += UnHandledExceptionHandler;
Application.ThreadException += ThreadExceptionHandler;


UnhandledExceptionはアプリケーションドメインレベルなので1回のみの登録でOKです。Application.ThreadExceptionはスレッド毎に独立しているのでスレッド毎に登録してください。ThreadExceptionはスレッドローカルストレージにあると考えてください。これらは、一般的にアプリケーションの初期処理で実装します。

  • UIを持たないスレッドの場合

以下の様な新規にスレッドを起こして実行する場合は、スレッドのメインルーチン全体をTry Catchで囲ってください。また、念のためThreadExceptionを登録します。



private void Button2_Click(System.Object sender, System.EventArgs e)
{
System.Threading.Thread t = new System.Threading.Thread(ThreadStart);
t.Start();
}

private void ThreadStart()
{

Application.ThreadException += ThreadExceptionHandler;

try {
throw new NullReferenceException();
} catch (Exception ex) {
MessageBox.Show(ex.Message);
}

}

  • UIスレッドだけど、新規スレッドから実行する場合

忘れやすいのは新規スレッドでの既定例外の設定です。Application.ThreadExceptionはスレッド単位なので新規スレッドを作成した場合は忘れずに設定します。



Private Sub Button3_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button3.Click

Dim t As New Threading.Thread(AddressOf ThreadUIStart)
t.Start()

End Sub

Private Sub ThreadUIStart()

AddHandler Application.ThreadException, AddressOf ThreadExceptionHandler

Try

Dim f As New Form2
f.ShowDialog()

Catch ex As Exception

MessageBox.Show(ex.Message)

End Try

End Sub


まとめ

  1. アプリケーション起動時にUnhandledExceptionを設定
  2. スレッド単位でThreadException設定
  3. スレッドを新規に作成した場合は、メインルーチンをTry Catchで囲む。


上記が基本ルールです。


ここで、スレッド毎にThreadExceptionを設定するならUnhandledExceptionは要らないのでは?UIを持たないスレッドではTry Catchで囲むならThreadExceptionは不要では?という疑問が出てきます。私も、不要だと思います。(UIを持たないスレッドでTryCatchなしでThreadExceptionの場合のみ設定した場合だと上記のダイアログを出してアプリケーションは停止してしまいます。)


私は念のため設定していますが、実際に動作したケースを見た事はありません。ですが、保険のために設定しておいて損はないと思います。それに、「まとめ」の3ルールに例外を設けて運用するより、単純化されて良いのではないかと思います。


それと小ネタですが、UnhandledExceptionとThreadExceptionは同一スレッド内で何回呼び出しても例外発生時に複数回呼び出されることはありません。通常、イベントハンドラは呼び出した回数だけイベントが発生するものですが、少し特殊な取扱いのようです。



using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading

namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}

private void Button1_Click(System.Object sender, System.EventArgs e)
{
throw new NullReferenceException();

}


private void Button2_Click(System.Object sender, System.EventArgs e)
{
System.Threading.Thread t = new System.Threading.Thread(ThreadStart);
t.Start();

}


private void ThreadStart()
{
Application.ThreadException += ThreadExceptionHandler;


try
{
throw new NullReferenceException();


}
catch (Exception ex)
{
MessageBox.Show(ex.Message);

}

}


private void Button3_Click(System.Object sender, System.EventArgs e)
{
System.Threading.Thread t = new System.Threading.Thread(ThreadUIStart);
t.Start();

}


private void ThreadUIStart()
{
Application.ThreadException += ThreadExceptionHandler;


try
{
Form1 f = new Form1();
f.ShowDialog();


}
catch (Exception ex)
{
MessageBox.Show(ex.Message);

}

}

private void Form1_Load(System.Object sender, System.EventArgs e)
{
Thread.GetDomain().UnhandledException += UnHandledExceptionHandler;
Application.ThreadException += ThreadExceptionHandler;
}


public static void UnHandledExceptionHandler(object sender, UnhandledExceptionEventArgs e)
{
Exception ex = (Exception)e.ExceptionObject;
MessageBox.Show(ex.Message);

}


public static void ThreadExceptionHandler(object sender, System.Threading.ThreadExceptionEventArgs e)
{
Exception ex = (Exception)e.Exception;
MessageBox.Show(ex.Message);

}

}
}