例外の処理時には何らかの理由で、キャッチした例外をリスローしなければならないときがある。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」メソッドが正しくレポートされている。
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」メソッドが正しくレポートされずに、「MethodA2」メソッドで例外が発生したかのようにレポートされる。
スタックトレースを失わないように、キャッチした例外を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の方に、例外を発生させた「MethodB」メソッドが正しくレポートされている。
例外をリスローするには、引数を付けずに「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.