例外の回収

例外処理は、30年以上前から主流のプログラミング言語で登場していましたが、今日でも例外を使用することを恐れる開発者に会うことができます。 コンストラクターで例外をスローすると、壊れやすいカルマにダメージを与え、狂ったインド人の群れによって書かれた20年前のコードをサポートする形で罰によって追い越されると考える人もいます。 一部はまだC言語の時代にとどまっており、C#言語でさえも、magic病者は例外を見つけ出し、本物のサムライはそれなしでできると信じて、マジックナンバーまたはストリングの形式でリターンコードを集中的に使用します。 (あなたと私は、本物のサムライは「サムライの原則」に従い、リターンコードを使用しないことを知っています)。



特定のプラットフォームおよびプログラミング言語により、さらに複雑さが増します。 今日のC#開発者インタビューで、例外の処理に関しては、疑問が必ず発生します。「 throw構造を使用して例外をスローすることの違いは何ですか 投げる; ?」 これはひどいボタンアコーディオンであり、ほとんどの開発者はこの質問に対する正しい答えを長い間知っていますが、実際のコードで「非コーシャ」バージョンを見つけることは非常に簡単です。



.NETプラットフォームの特定の機能は、ある場所で例外をキャッチし、別の場所で例外を生成する方法が存在しないことです(むしろ、すぐにわかるように、存在しませんでした)。 ビジネスアプリケーションまたはライブラリの開発者がこのような問題に直面した場合、非常に簡単な方法で解決されました。元の例外はネストされた例外として別のオブジェクトにラップされ、新しい例外がスローされました。







そのような例を見てみましょう。 CustomExceptionと呼ばれるカスタム例外クラスと、この例外をスローするコンストラクターの単純なクラスSampleClassがあるとします。



//   ,  ,   class CustomException : Exception { } class SampleClass { //   , //     ,    public SampleClass() { throw new CustomException(); } }
      
      







次に、ジェネリックメソッドを使用してこのクラスのオブジェクトを作成し、このクラスのシングルトンを作成します。



 //   ,    public static T CreateInstance<T>() where T : new() { return new T(); } //    class SampleClassSingleton { private static SampleClass _instance = new SampleClass(); static SampleClassSingleton() { } public static SampleClass Instance { get { return _instance; } } }
      
      







問題は、 CreateInstance < SampleClass>()メソッドを呼び出すとき、またはSampleClassSingletonにアクセスするときに、どのcatchブロックが実行されるかです。 インスタンス



 try { CreateInstance<SampleClass>(); //  var instance = SampleClassSingleton.Instance; } catch (CustomException e) { Console.WriteLine(e); } catch (Exception e) { Console.WriteLine(e); }
      
      







このコードは誰にとっても素晴らしい啓示になるとは思いませんが、どちらの場合も「期待される」 catchブロック(CustomException e)は実行されず、代わりにcatch(Exception e)ブロックが実行されます。



最初のケース(つまり、汎用のCreateInstanceメソッドを使用)では、デフォルトのコンストラクターは直接呼び出されず、代わりにActivator.CreateInstanceが使用されます。これにより、元の例外がTargetInvocationExceptionにラップされます。 TargetInvocationExceptionは例外処理の最初のブロックに適合しないため、 catch(Exception e)ブロックがトリガーされるのはこのためです



警告

e.Messageのみをログに書き込まないでください。実稼働サーバーがダウンした場合、この情報によって寒くも暑くもなりません。最も重要な情報は、含まれている例外の1つに潜む可能性があるためです。 .Net Frameworkの一部としても、元の例外をラップしてネストされた例外としてスローする場所はたくさんあります。もちろん、必要な例外を12個のネストされた例外でラップできるカスタムコードは言うまでもありません。



シングルトンを使用する場合、状況は似ています: _instanceフィールドは静的コンストラクターで初期化されるため、実行中に発生する例外はタイプを「無効」にします。 その結果、この型に対する以降のすべての呼び出しは、対応するネストされた例外とともにTypeLoadExceptionをスローします



空の静的コンストラクターが必要な理由と、その欠如が引き起こす可能性のある問題の詳細については、シングルトンおよび静的コンストラクターについてを参照してください



どちらの場合の動作も十分に文書化されており、よく知られているため、一度バグと見なすことができれば、今では機能です。 しかし、残念ながら、この状況を修正することはできません。 CreateInstanceメソッドで例外をキャッチし、ネストされた例外をスローしようとしても、呼び出しスタックは失われ、結果の例外の有用性は大幅に低下します。



 public static T CreateInstance<T>() where T : new() { try { return new T(); } catch (TargetInvocationException e) { //    ,    ,   ! throw e.InnerException; } }
      
      







instance _ instanceを作成するときに例外を保存し、 Instanceプロパティにアクセスするときに例外をスローしようとすると、同様の問題が発生します。 (はい、遅延初期化はこの特定の問題を解決しますが、今ではそれについてではありません)。



ただし、この問題の解決に役立つクラスが.Net Framework 4.5に登場しました。 これはExceptionDispatchInfoクラスで、元の例外を保存してから、コールスタックに関する情報を失うことなく再度スローすることができます。 もちろん、そのアプリケーションの主なポイントは、静的コンストラクターまたはActivator.CreateInstanceメソッドの「直線化」とは関係ありません。 その主なタスクは、あるスレッドで例外が発生し、別のスレッドでスローされた場合の非同期性とマルチスレッドの問題を解決することです。



次のコードを見てみましょう。



 Task<int> task = Task<int>.Factory.StartNew(() => { throw new CustomException(); }); try { int result = task.Result; } catch (CustomException e) { // ,      :( Console.WriteLine("CustomException caught: " + e); } catch (AggregateException e) { //      ! var inner = e.GetBaseException(); Console.WriteLine("Aggregate excpetion caught: " + inner); }
      
      







「タスク」が例外で失敗した場合、元の例外(この例ではCustomException )はAggregateExceptionにラップされます。 この振る舞いにはいくつかの理由があります:まず、タスクは本質的に単一の結果を持ついくつかの長期非同期操作のシェルですが、場合によっては、1つのタスクの結果が複数のタスクの並列実行の結果に基づいていることがあります。 たとえば、 Taskを使用して複数のタスクを1つにまとめることができます。 WaitAllまたは継続を使用して複数のタスクをバインドできます。 この動作の2番目の理由は、呼び出しスタックを歪めることなく初期例外を転送できないことです。



ただし、非同期で作業するためのC#5.0の新機能の登場後、優先順位は多少変更されました。 awaitキーワードとasyncキーワードの主な機能の1つは、同期コードを非同期に変換する単純さですが、実行スレッドを「まっすぐにする」ことに加えて(タスクが必要な場合でも)、例外を処理するタスクを解決する必要があります。 したがって、 awaitキーワードを使用しタスクの結果を受け取ると、元のAggregateExceptionがスローされ、元の例外がスローされます。



 public static async void SimpleTask() { Task<int> task = Task<int>.Factory.StartNew(() => { throw new CustomException(); }); try { // await «»      //    ,   AggregateExcpetion! int result = await task; } catch (CustomException e) { //    ,       Console.WriteLine("CustomException caught: " + e); } }
      
      









asyncawaitなどのC#5.0言語の機能に詳しくない場合は、記事「 C#5での非同期操作 」を利用してこのギャップを埋めることができます。



既に述べたように、この動作は新しいExceptionDispatchInfoクラスを使用して実装されます。これにより、元の例外を保存し、後で新しいスレッドでも例外をスローできます。 さらに、これは非常に単純に実装されているため、これを自分で行うことは難しくありません。



開始するには、GetResult拡張メソッド使用するT ask < T >クラスのクラスを作成します。これは、 Resultプロパティに非常に似ていますが、 AggregateExcpetionを「ストレート」にし、呼び出しスタックを失うことなくネストされた例外をスローします。





この動作は、 タスク < T >を使用して.Net Framework 4.5の一部として既に実装されています GetAwaiter ()。 GetResult ()ですが、それについて忘れて、自分で同じことをしましょう。



ExceptionDispatchInfoクラスの使用は非常に簡単です。静的なCaptureメソッドを使用して1つの場所で例外を取得し、 Throwメソッドを使用して別の場所(場合によっては別のスレッド)で例外をスローします。



 static class TaskExtensions { public static T GetResult<T>(this Task<T> task) { try { T result = task.Result; return result; } catch (AggregateException e) { ExceptionDispatchInfo di = ExceptionDispatchInfo.Capture(e.InnerException); di.Throw(); return default(T); } } }
      
      







ここで、前のコードフラグメントを変更してtaskを置き換えた場合 Taskメソッドの呼び出し結果 GetResultを使用すると、 AggregateExceptionの代わりに特定の種類の例外をキャッチできます。



 Task<int> task = Task<int>.Factory.StartNew(() => { throw new CustomException(); }); try { int result = task.GetResult(); } catch (CustomException e) { //     CustomException,   AggregateException Console.WriteLine("CustomException caught: " + e); }
      
      







同様に、 CreateInstanceメソッドを変更して、 TargetInfocationExceptionをキャッチし、ネストされた例外をスローできます。



 public static T CreateInstance<T>() where T : new() { try { var t = new T(); return t; } catch (TargetInvocationException e) { //     ExceptionDispatchInfo ExceptionDispatchInfo di = ExceptionDispatchInfo.Capture(e.InnerException); //        di.Throw(); //   ,  di.Throws()   ,  //        ,     //   return default(T); } }
      
      







ここで、 Main関数からこのメソッドを呼び出そうとすると、通常の実行スタックで適切な例外が発生します。



ConsoleApplication1.CustomException: Exception of type 'ConsoleApplication1.CustomException' was thrown.<br>at ConsoleApplication1.SampleClass..ctor() in <br>c:\Projects\ConsoleApplication1\ConsoleApplication1\Program.cs:line 19<br> --- End of stack trace from previous location where exception was thrown ---<br> at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()<br>at ConsoleApplication1.Program.CreateInstance[T]() in c<br>\Projects\ConsoleApplication1\ConsoleApplication1\Program.cs:<br>line 50<br>at ConsoleApplication1.Program.Main(String[] args) in c:\ \Projects\ConsoleApplication1\ConsoleApplication1\Program.cs:line 63









シングルトンクラスはこの方法で実装できます。



ExceptionDispatchInfoクラスは、.Net Frameworkの新しいバージョンのキラー機能とは考えられませんが、非同期の時代の到来により、間違いなく価値のあるスコープが見つかるでしょう。 そのため、たとえば、リアクティブ拡張機能のライブラリで既に使用されており、 awaitパターンを実装してます(この実装は実験リリースでのみ使用可能です)。このパターンを独自に実装したい人は誰でも使用できます。



All Articles