構文:キャッチした例外をリスローするには?[C#/VB]:.NET TIPS
例外の処理時には何らかの理由で、キャッチした例外をリスローしなければならないときがある。C#やVBでこれを適切に行う方法を解説する。
キャッチした例外をリスロー(再スロー)する場合がある。例えば、キャッチしてリカバリーを試みたが成功しなかったときや、キャッチした例外を解析してみないとリカバリーできるかどうか分からないときなどに、そのキャッチした例外を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
一部、行頭に行番号を付けてある。実際に試すときには、行番号は入力しないでほしい。
「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」メソッドが正しくレポートされている。
引数を付けてしまうと?
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
先ほどのサンプルコードの一部をこのように書き換えた。
一部、行頭に行番号を付けてある。実際に試すときには、行番号は入力しないでほしい。
「MethodB」メソッド(先のサンプルコードと同じなのでここでは省略)の中で発生したApplicationException例外を、「MethodA2」メソッドの中でキャッチし、今度は例外オブジェクトを付けてスローしている(Javaとは異なり、.NETではリスローにならない)。それを「Main」メソッドでキャッチして、例外のスタックトレースをコンソールに出力している。また、次項で述べることと比較するために、InnerExceptionの存在をチェックして出力している。
上のサンプルコードをデバッグ実行すると、次の画像のようにスタックトレースが出力される。例外を発生させた「MethodB」メソッドが正しくレポートされずに、「MethodA2」メソッドで例外が発生したかのようにレポートされる。
引数を付けてスローしたときのスタックトレース出力例(上: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
先ほどのサンプルコードの一部をこのように書き換えた。
一部、行頭に行番号を付けてある。実際に試すときには、行番号は入力しないでほしい。
このようにすれば真の例外発生箇所を特定できるが、(例外の種類を変えるという要件がない限り)リスローに比べてコードが複雑になるだけでメリットはない。
「MethodB」メソッド(先のサンプルコードと同じなのでここでは省略)の中で発生したApplicationException例外を、「MethodA3」メソッドの中でキャッチし、新しく作った例外オブジェクトのInnerExceptionプロパティにそれをセットしてからスローしている。それを「Main」メソッドでキャッチして、例外のスタックトレースを(InnerExceptionの分まで)コンソールに出力している。
上のサンプルコードをデバッグ実行すると、次の画像のようにスタックトレースが出力される。InnerExceptionの方に、例外を発生させた「MethodB」メソッドが正しくレポートされている。
例外を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 Framework 1.0以降(サンプルコードにはそれ以降の構文も含む)
カテゴリ:C# 処理対象:言語構文
カテゴリ:Visual Basic .NET 処理対象:言語構文
関連TIPS:Visual Studioでコンソール・アプリケーションのデバッグ実行時にコマンド・プロンプトを閉じないようにするには?
関連TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]
関連TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?
関連TIPS:構文:条件を指定して例外をキャッチするには?[C# 6/VB]
関連TIPS:WPF:例外をまとめてトラップするには?[C#/VB]
関連TIPS:適切に処理されなかった例外をキャッチするには?(Windowsフォーム)
Copyright© Digital Advantage Corp. All Rights Reserved.