C#での例外の再スロー

プロジェクトメンバーが書いたC#のソースコードで適切でない例外の再スローの実装がありました。今回は、C#におけるcatchブロック内でのthrowについてです。

例外の再スロー(問題ありのコード)

例外の再スローで適切でないコードというのは以下のコードです。


try
{
    ...
}
catch (Exception ex)
{
    ...
    throw ex;
}

例外をキャッチした後、何らかの処理をした後に、キャッチしたときの例外変数を再スローしています。

何が適切でないかわかりますか?

例外をそのまま再スローしたい場合は、通常は以下のように書きます。

throw;

しかし、適切でない再スローコードでは、以下のようになっています。

throw ex;

キャッチしたときの例外変数をthrowキーワードに渡して、再スローしています。どちらも、呼び出し元に例外を投げることができますが、一つだけ大きな違いがあります。StackTraceプロパティの値です。

前者では、もともとの例外のStackTraceの値がそのまま維持した状態で例外が投げられます。

後者では、もともとの例外のStackTraceの値は破棄され、StackTraceの値が再スローしたとこからの値のみになります。

後者の方法では、例外の情報が欠落してしまうので、StackTraceプロパティの値をクリアしたいとき以外は使うべき実装ではありません。

再スローの実際の挙動

実際の挙動を確認してみます。

throwキーワードに例外インスタンスを渡す場合

throwキーワードに元の例外の例外インスタンスを渡す場合の例として、以下のコードを用意します。

public MainWindow()
{
    InitializeComponent();
    try
    {
        try
        {
            Encoding.GetEncoding("Shift_JIS");
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine(ex.ToString());
            throw ex;
        }
    }
    catch (Exception ex2)
    {
        System.Diagnostics.Debug.WriteLine(ex2.ToString());
    }
    ....

このコードは、.NET Coreで実行すると、 Encoding.GetEncoding(“Shift_JIS”)の呼び出しの内部で例外が発生します。なお、.NET Frameworkで実行した場合は例外が発生しません。

このコードを実行すると、Debug.WriteLine()で以下のように出力されます。

例外がスローされました: 'System.ArgumentException' (System.Private.CoreLib.dll の中)
System.ArgumentException: 'Shift_JIS' is not a supported encoding name. For information on defining a custom encoding, see the documentation for the Encoding.RegisterProvider method. (Parameter 'name')
   at System.Text.EncodingTable.InternalGetCodePageFromName(String name)
   at System.Text.EncodingTable.GetCodePageFromName(String name)
   at System.Text.Encoding.GetEncoding(String name)
   at WpfAppNetCore.MainWindow..ctor() in C:\ExceptionTest\WpfAppNetCore\MainWindow.xaml.cs:line 32

例外がスローされました: 'System.ArgumentException' (WpfAppNetCore.dll の中)
System.ArgumentException: 'Shift_JIS' is not a supported encoding name. For information on defining a custom encoding, see the documentation for the Encoding.RegisterProvider method. (Parameter 'name')
   at WpfAppNetCore.MainWindow..ctor() in C:\ExceptionTest\WpfAppNetCore\MainWindow.xaml.cs:line 37

内側のcatchブロック内では、例外インスタンスexのStackTraceプロパティの値は、例外が発生した位置がわかるものとなっています。

しかし、外側のcatchブロック内では、 例外インスタンスex2は、StackTraceプロパティ以外は元の例外の情報が保持されています。しかし、StackTraceプロパティの値は、元の例外の発生箇所がわかる値ではなく、再スローしたところからの値となっています。

結果として、例外インスタンスex2の値から、例外の正確な発生箇所がわからなくなっています。

throwキーワードに例外インスタンスを渡さない場合

throwキーワードに元の例外の例外インスタンスを渡さない場合の例として、以下のコードを用意します。

public MainWindow()
{
    InitializeComponent();
    try
    {
        try
        {
            Encoding.GetEncoding("Shift_JIS");
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine(ex.ToString());
            throw;
        }
    }
    catch (Exception ex2)
    {
        System.Diagnostics.Debug.WriteLine(ex2.ToString());
    }
    ....

先ほどのコードとの違いは、内側のthrowキーワードに、例外インスタンスexを渡していないだけです。それ以外のコードは全く同じです。

このコードを実行すると、Debug.WriteLine()で以下のように出力されます。

例外がスローされました: 'System.ArgumentException' (System.Private.CoreLib.dll の中)
System.ArgumentException: 'Shift_JIS' is not a supported encoding name. For information on defining a custom encoding, see the documentation for the Encoding.RegisterProvider method. (Parameter 'name')
   at System.Text.EncodingTable.InternalGetCodePageFromName(String name)
   at System.Text.EncodingTable.GetCodePageFromName(String name)
   at System.Text.Encoding.GetEncoding(String name)
   at WpfAppNetCore.MainWindow..ctor() in C:\ExceptionTest\WpfAppNetCore\MainWindow.xaml.cs:line 32

例外がスローされました: 'System.ArgumentException' (WpfAppNetCore.dll の中)
System.ArgumentException: 'Shift_JIS' is not a supported encoding name. For information on defining a custom encoding, see the documentation for the Encoding.RegisterProvider method. (Parameter 'name')
   at System.Text.EncodingTable.InternalGetCodePageFromName(String name)
   at System.Text.EncodingTable.GetCodePageFromName(String name)
   at System.Text.Encoding.GetEncoding(String name)
   at WpfAppNetCore.MainWindow..ctor() in C:\ExceptionTest\WpfAppNetCore\MainWindow.xaml.cs:line 32

内側のcatchブロック内では、例外インスタンスexのStackTraceプロパティの値は、例外が発生した位置がわかるものとなっています。

外側のcatchブロック内では、 例外インスタンスex2は、StackTraceプロパティも含めて元の例外の情報が保持されています。

結果として、例外インスタンスex2の値から、例外の正確な発生箇所がわかります。

この結果を見てわかるように、同じ例外を再スローするときは、catchした例外インスタンスを渡してはいけないことがわかります。

catchブロックで新たな例外インスタンスをスロー

では、同じ例外を再スローするのではなく、新たに作成した例外インスタンスをスローする場合はどうすればよいのでしょうか?

たとえば、”test”という例外メッセージを呼び出し側に伝えたい場合です。

catchブロック内で、単純に throw new Exception(“test”); として、新しい例外インスタンスをthrowすることができます。

ただ、方法では、元の例外の情報がStackTraceプロパティだけなく、すべてがなくなるという問題があります。StackTraceプロパティの情報も含む元の例外のすべての情報を欠落させず新しい例外をthrowしたい場合は、どうすればよいでしょうか。

簡単です、InnerExceptionプロパティに元の例外をセットすればよいのです。InnerExceptionプロパティに元の例外を設定するには、新しい例外を作成するときに、コンストラクタの引数に設定します。

throw new Exception("test", ex);

throwキーワードに元の例外の情報持つ新しい例外インスタンスを渡す場合の例として、以下のコードを用意します。

public MainWindow()
{
    InitializeComponent();
    try
    {
        try
        {
            Encoding.GetEncoding("Shift_JIS");
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine(ex.ToString());
            throw new Exception("test", ex);
        }
    }
    catch (Exception ex2)
    {
        System.Diagnostics.Debug.WriteLine(ex2.ToString());
    }
    ....

先ほどのコードとの違いは、内側のthrowキーワードに新しい例外インスタンスを渡しているだけです。それ以外のコードは全く同じです。

このコードを実行すると、Debug.WriteLine()で以下のように出力されます。

例外がスローされました: 'System.ArgumentException' (System.Private.CoreLib.dll の中)
System.ArgumentException: 'Shift_JIS' is not a supported encoding name. For information on defining a custom encoding, see the documentation for the Encoding.RegisterProvider method. (Parameter 'name')
   at System.Text.EncodingTable.InternalGetCodePageFromName(String name)
   at System.Text.EncodingTable.GetCodePageFromName(String name)
   at System.Text.Encoding.GetEncoding(String name)
   at WpfAppNetCore.MainWindow..ctor() in C:\ExceptionTest\WpfAppNetCore\MainWindow.xaml.cs:line 32

例外がスローされました: 'System.Exception' (WpfAppNetCore.dll の中)
System.Exception: test
 ---> System.ArgumentException: 'Shift_JIS' is not a supported encoding name. For information on defining a custom encoding, see the documentation for the Encoding.RegisterProvider method. (Parameter 'name')
   at System.Text.EncodingTable.InternalGetCodePageFromName(String name)
   at System.Text.EncodingTable.GetCodePageFromName(String name)
   at System.Text.Encoding.GetEncoding(String name)
   at WpfAppNetCore.MainWindow..ctor() in C:\ExceptionTest\WpfAppNetCore\MainWindow.xaml.cs:line 32
   --- End of inner exception stack trace ---
   at WpfAppNetCore.MainWindow..ctor() in C:\ExceptionTest\WpfAppNetCore\MainWindow.xaml.cs:line 37

内側のcatchブロック内では、例外インスタンスexのStackTraceプロパティの値は、例外が発生した位置がわかるものとなっています。

外側のcatchブロック内では、 例外インスタンスex2は、MessageプロパティやStackTraceプロパティは新しいインスタンスをthrowしたところからの情報です。しかし、InnerExceptionプロパティに元になった例外の情報がすべて含まれています。

結果として、例外インスタンスex2の値から、新たな例外インスタンスで追加された例外情報がわかりつつ、元の例外の正確な発生箇所がわかります。

この結果を見てわかるように、catchブロック内で新しい例外をスローするときは、元の例外の情報をInnerExceptionプロパティとして渡すことよいことがわかります。情報が多いほうが、プログラムのデバッグが容易になるからです。

なお、意図的に内部例外の情報を呼び出し側へ渡したくない場合は、InnerExceptionプロパティに設定をしません。


以上、今回の投稿は、C#におけるcatchブロック内でのthrowに関する話題でした。

コメントを残す