構文:キャッチした例外をリスローするには?[C#/VB].NET TIPS

例外の処理時には何らかの理由で、キャッチした例外をリスローしなければならないときがある。C#やVBでこれを適切に行う方法を解説する。

» 2017年01月11日 05時00分 公開
[山本康彦BluewaterSoft/Microsoft MVP for Windows Development]
「.NET TIPS」のインデックス

連載目次

 キャッチした例外をリスロー(再スロー)する場合がある。例えば、キャッチしてリカバリーを試みたが成功しなかったときや、キャッチした例外を解析してみないとリカバリーできるかどうか分からないときなどに、そのキャッチした例外をcatchブロックの中から再びスローする(=リスローする)ことになる。

 リスローの記述は簡単なのだが、間違えやすい(特にJavaでのリスローと混同している人が多いように思われる)。本稿では、リスローの書き方を説明するとともに、よくない例も紹介する。

例外をリスローするには?

 結論からいってしまうと、「引数なしで「throw;」(C#)/「Throw」(VB)とだけ書けば」よい。

 Javaではリスローするときに「throw ex;」などと引数を付けて書くが、.NETでは引数を付けてはいけないのである。

 例えば、次に示すコードの「MethodA1」メソッドの中では正しく例外をリスローしている。

……省略……
using static System.Console;

namespace dotNetTips1172
{
  class Program
  {
    static void Main(string[] args)
    {
      WriteLine("=== throw - 正しく例外発生箇所が分かる ===");
      try
      {
17:     MethodA1();
      }
      catch (Exception ex)
      {
        WriteLine(ex.StackTrace);
      }

      ……省略……

#if DEBUG
      ReadKey(); //デバッグ実行時にコンソールを閉じない
#endif
    }

    static void MethodA1()
    {
      try
      {
        MethodB();
      }
      catch
      {
69:     throw; // リスロー
      }
    }

    ……省略……

    static void MethodB()
    {
99:   throw new ApplicationException();
    }
  }
}

Imports System.Console

Module Module1

  Sub Main()
    WriteLine("=== throw - 正しく例外発生箇所が分かる ===")
    Try
8:    MethodA1()
    Catch ex As Exception
      WriteLine(ex.StackTrace)
    End Try

    ……省略……

#If DEBUG Then
    ReadKey() 'デバッグ実行時にコンソールを閉じない
#End If
  End Sub

  Private Sub MethodA1()
    Try
      MethodB()
    Catch
46:   Throw ' リスロー
    End Try
  End Sub

  ……省略……

  Private Sub MethodB()
67: Throw New ApplicationException()
  End Sub
End Module

例外をリスローするコンソールアプリの例(上:C#、下:VB)
一部、行頭に行番号を付けてある。実際に試すときには、行番号は入力しないでほしい。
「MethodB」メソッドの中で発生したApplicationException例外を、「MethodA1」メソッドの中でキャッチしてリスローしている。それを「Main」メソッドでキャッチして、例外のスタックトレースをコンソールに出力している。
なお、C#コードの冒頭にある「using static System.Console;」という書き方は、Visual Studio 2015からのものだ。詳しくは、「.NET TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]」をご覧いただきたい。同様な機能がVBには以前から備わっており、「.NET TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?」で解説している。
また、「Main」メソッド末尾にReadKeyメソッドを置く意味は、「.NET TIPS:Visual Studioでコンソール・アプリケーションのデバッグ実行時にコマンド・プロンプトを閉じないようにするには?」をご覧いただきたい。

 上のサンプルコードをデバッグ実行すると、次の画像のようにスタックトレースが出力される。例外を発生させた「MethodB」メソッドが正しくレポートされている。

正しくリスローしたときのスタックトレース出力例(上:C#、下:VB)
正しくリスローしたときのスタックトレース出力例(上:C#、下:VB) 正しくリスローしたときのスタックトレース出力例(上:C#、下:VB)
スタックトレースの先頭に、例外が「MethodB」メソッドで発生したことがレポートされている。

引数を付けてしまうと?

 Javaでリスローするときのようにthrowの後にキャッチした例外オブジェクトを書くと、.NETではそこで新しく例外が発生したことになる。そこでキャッチしたときまでのスタックトレースが失われてしまい、例外を引き起こした「犯人」が分からなくなってしまうのである。

 先ほどのサンプルコードを、次のコードのように書き換えてみよう。「MethodA2」メソッドの中で、引数付きのthrowを書いている(「MethodB」メソッドは前と同じなので省略)。

……省略……

class Program
{
  static void Main(string[] args)
  {
    ……省略……

    WriteLine("=== throw ex - 例外発生箇所が変わる ===");
    try
    {
28:   MethodA2();
    }
    catch (Exception ex)
    {
      WriteLine(ex.StackTrace);
      if(ex.InnerException == null)
      {
        // InnerExceptionには何も入っていない
        WriteLine($"--- No InnerException");
      }
    }
    ……省略……
  }

  ……省略……

  static void MethodA2()
  {
    try
    {
      MethodB();
    }
    catch (Exception ex)
    {
81:   throw ex; // 引数付きでスロー
    }
  }

  ……省略(MethodB)……
}

……省略……

Module Module1
  Sub Main()
    ……省略……

    WriteLine("=== throw ex - 例外発生箇所が変わる ===")
    Try
16:   MethodA2()
    Catch ex As Exception
      WriteLine(ex.StackTrace)
      If (ex.InnerException Is Nothing) Then
        ' InnerExceptionには何も入っていない
        WriteLine($"--- No InnerException")
      End If
    End Try

    ……省略……
  End Sub

  ……省略……

  Private Sub MethodA2()
    Try
      MethodB()
    Catch ex As Exception
54:   Throw ex ' 引数付きでスロー
    End Try
  End Sub

  ……省略(MethodB)……
End Module

例外を出し直しているよくない例(上:C#、下:VB)
先ほどのサンプルコードの一部をこのように書き換えた。
一部、行頭に行番号を付けてある。実際に試すときには、行番号は入力しないでほしい。
「MethodB」メソッド(先のサンプルコードと同じなのでここでは省略)の中で発生したApplicationException例外を、「MethodA2」メソッドの中でキャッチし、今度は例外オブジェクトを付けてスローしている(Javaとは異なり、.NETではリスローにならない)。それを「Main」メソッドでキャッチして、例外のスタックトレースをコンソールに出力している。また、次項で述べることと比較するために、InnerExceptionの存在をチェックして出力している。

 上のサンプルコードをデバッグ実行すると、次の画像のようにスタックトレースが出力される。例外を発生させた「MethodB」メソッドが正しくレポートされずに、「MethodA2」メソッドで例外が発生したかのようにレポートされる。

引数を付けてスローしたときのスタックトレース出力例(C#)
引数を付けてスローしたときのスタックトレース出力例(VB) 引数を付けてスローしたときのスタックトレース出力例(上:C#、下:VB)
スタックトレースの先頭に、例外が「MethodA2」メソッドで発生したとレポートされている。真の例外発生箇所である「MethodB」メソッドは、どこにも出ていない。
この例では「MethodA2」メソッドのtryブロックには(「MethodB」メソッドを呼び出している)1行しか書いていないので、これでも本当の例外発生箇所は「MethodB」メソッドの中にあると分かる。実際にはtryブロック内に複数行のコードを記述することも多いので、本当の例外発生箇所を特定できなくなってしまうだろう。

別法:InnerExceptionを使う

 スタックトレースを失わないように、キャッチした例外をInnerExceptionにセットしてからスローする方法もある。

 これはごくたまに見かけることのある書き方であるが、別の例外に置き換えるためでなければ、catchブロックの中が複雑になるだけでメリットはない(恐らくは、先ほどの引数付きスローでスタックトレースが正しく取れなかった対策として実装したものだろう)。

 先ほどのサンプルコードを、次のコードのように書き換えてみよう。「MethodA3」メソッドの中で、新しく例外を作ってthrowしている(「MethodB」メソッドは前と同じなので省略)。

……省略……

class Program
{
  static void Main(string[] args)
  {
    ……省略……

    WriteLine("=== throw new Exception - InnerExceptionに元の例外を保持する ===");
    try
    {
44:   MethodA3();
    }
    catch (Exception ex)
    {
      WriteLine(ex.StackTrace);
      if (ex.InnerException != null)
      {
        WriteLine($"--- InnerException ({ex.InnerException.GetType().Name})");
        WriteLine(ex.InnerException.StackTrace);
      }
    }
    ……省略……
  }

  ……省略……

  static void MethodA3()
  {
    try
    {
89:   MethodB();
    }
    catch (Exception ex)
    {
93:   throw new ApplicationException("例外発生", ex);
    }
  }

  ……省略(MethodB)……
}

……省略……

Module Module1

  Sub Main()
    ……省略……

    WriteLine("=== throw new Exception - InnerExceptionに元の例外を保持する ===")
    Try
28:   MethodA3()
    Catch ex As Exception
      WriteLine(ex.StackTrace)
      If (ex.InnerException IsNot Nothing) Then
        WriteLine($"--- InnerException ({ex.InnerException.GetType().Name})")
        WriteLine(ex.InnerException.StackTrace)
      End If
    End Try

    ……省略……
  End Sub

  ……省略……

  Private Sub MethodA3()
    Try
60:   MethodB()
    Catch ex As Exception
62:   Throw New ApplicationException("例外発生", ex)
    End Try
  End Sub

  ……省略(MethodB)……
End Module

キャッチした例外をInnerExceptionにセットしてスローするコード例(上:C#、下:VB)
先ほどのサンプルコードの一部をこのように書き換えた。
一部、行頭に行番号を付けてある。実際に試すときには、行番号は入力しないでほしい。
このようにすれば真の例外発生箇所を特定できるが、(例外の種類を変えるという要件がない限り)リスローに比べてコードが複雑になるだけでメリットはない。
「MethodB」メソッド(先のサンプルコードと同じなのでここでは省略)の中で発生したApplicationException例外を、「MethodA3」メソッドの中でキャッチし、新しく作った例外オブジェクトのInnerExceptionプロパティにそれをセットしてからスローしている。それを「Main」メソッドでキャッチして、例外のスタックトレースを(InnerExceptionの分まで)コンソールに出力している。

 上のサンプルコードをデバッグ実行すると、次の画像のようにスタックトレースが出力される。InnerExceptionの方に、例外を発生させた「MethodB」メソッドが正しくレポートされている。

例外をInnerExceptionに入れてスローしたときのスタックトレース出力例(C#)
例外をInnerExceptionに入れてスローしたときのスタックトレース出力例(VB) 例外をInnerExceptionに入れてスローしたときのスタックトレース出力例(上:C#、下:VB)
スタックトレースの先頭には、例外が「MethodA3」メソッドで発生したとレポートされている。InnerExceptionのスタックトレースを見ると、真の例外発生箇所である「MethodB」メソッドがレポートされている。
このようにしてもスタックトレースを失わずに済むが、最初に示したリスローする方法に比べるとコードが複雑になっている。「MethodA3」メソッドでキャッチしたときに別の例外に置き換える必要がある場合は、キャッチした例外をこのようにしてInnerExceptionに入れるとよい。それ以外では、このような書き方をするメリットはないだろう。

まとめ

 例外をリスローするには、引数を付けずに「throw」とだけ書く。引数を付けるとスタックトレースが失われて例外の発生箇所が分からなくなるので、注意しよう。

 なお、catch句の後ろにwhen句を付けてキャッチする例外を絞り込むことで、そもそもリスローが不要なコードにできる場合も多い(「.NET TIPS:構文:条件を指定して例外をキャッチするには?[C# 6/VB]」を参照)。また、ロギングのためだけに例外をキャッチ(ロギング後にリスロー)する必要も、通常はそれほど多くないだろう。1カ所でまとめてキャッチしてロギングすればよいからだ(「.NET TIPS:WPF:例外をまとめてトラップするには?[C#/VB]」および「.NET TIPS:適切に処理されなかった例外をキャッチするには?」を参照)。つまり、リスローは簡単に書けるが、しかしそれは最後の手段なのである。

「.NET TIPS」のインデックス

.NET TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。