私が恐れていたことの 1 つは、例外を通常のオブジェクトとして処理して渡すと、例外を再度発生させて元のスタックトレースを保持できなくなることです。
しかし、それは、途中または最後にraise excn
.
コメントからすべてのアイデアを取り上げ、問題の 3 つの解決策としてここに示します。あなたにとって最も自然に感じるものを選んでください。
ExceptionDispatchInfo でスタック トレースをキャプチャする
次の例は、TeaDrivenDev の提案が実際に使用されていることを示していExceptionDispatchInfo.Capture
ます。
type Ex =
/// Capture exception (.NET 4.5+), keep the stack, add current stack.
/// This puts the origin point of the exception on top of the stacktrace.
/// It also adds a line in the trace:
/// "--- End of stack trace from previous location where exception was thrown ---"
static member inline throwCapture ex =
ExceptionDispatchInfo.Capture ex
|> fun disp -> disp.Throw()
failwith "Unreachable code reached."
元の質問 (置換) の例では、これにより次のトレースが作成されます ( 「--- 例外がスローされた前の場所からのスタック トレースの終わり ---」raise ex
の行に注意してください)。
System.DivideByZeroException : Attempted to divide by zero.
at Playful.Ex.Extern.calc(Int32 x, Int32 y) in R:\path\Ex.fs:line 118
at Playful.Ex.TestRes.testme@137-1.Invoke(Unit unitVar) in R:\path\Ex.fs:line 137
at Playful.Ex.Result.ResultBuilder.Run[b](FSharpFunc`2 f) in R:\path\Ex.fs:line 103
at Playful.Ex.Result.ResultBuilder.TryWith[a](FSharpFunc`2 body, FSharpFunc`2 handler) in R:\path\Ex.fs:line 105
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
at Playful.Ex.TestRes.testme() in R:\path\Ex.fs:line 146
at Playful.Ex.Tests.TryItOut() in R:\path\Ex.fs:line 153
スタックトレースを完全に保持する
.NET 4.5 を持っていない場合、またはトレースの途中に追加された行が気に入らない場合 ( 「--- 例外がスローされた前の場所からのスタック トレースの終わり ---」 )、保存できます。スタックに移動し、現在のトレースを一度に追加します。
私はTeaDrivenDevの解決策に従ってこの解決策を見つけ、Preserving stacktrace when rethrowing exceptions に遭遇しました。
type Ex =
/// Modify the exception, preserve the stacktrace and add the current stack, then throw (.NET 2.0+).
/// This puts the origin point of the exception on top of the stacktrace.
static member inline throwPreserve ex =
let preserveStackTrace =
typeof<Exception>.GetMethod("InternalPreserveStackTrace", BindingFlags.Instance ||| BindingFlags.NonPublic)
(ex, null)
|> preserveStackTrace.Invoke // alters the exn, preserves its stacktrace
|> ignore
raise ex
元の質問 (replace raise ex
) の例では、スタック トレースが適切に結合されており、例外の発生元が一番上にあるべき場所にあることがわかります。
System.DivideByZeroException : Attempted to divide by zero.
at Playful.Ex.Extern.calc(Int32 x, Int32 y) in R:\path\Ex.fs:line 118
at Playful.Ex.TestRes.testme@137-1.Invoke(Unit unitVar) in R:\path\Ex.fs:line 137
at Playful.Ex.Result.ResultBuilder.Run[b](FSharpFunc`2 f) in R:\path\Ex.fs:line 103
at Playful.Ex.Result.ResultBuilder.TryWith[a](FSharpFunc`2 body, FSharpFunc`2 handler) in R:\path\Ex.fs:line 105
at Microsoft.FSharp.Core.Operators.Raise[T](Exception exn)
at Playful.Ex.TestRes.testme() in R:\path\Ex.fs:line 146
at Playful.Ex.Tests.TryItOut() in R:\path\Ex.fs:line 153
例外を例外でラップする
これはFyodor Soikinによって提案されたもので、BCL で多くの場合に使用されているため、おそらく .NET の既定の方法です。ただし、多くの状況であまり役に立たないスタックトレースが発生し、深くネストされた関数で混乱を招く混乱したトレースにつながる可能性があります。
type Ex =
/// Wrap the exception, this will put the Core.Raise on top of the stacktrace.
/// This puts the origin of the exception somewhere in the middle when printed, or nested in the exception hierarchy.
static member inline throwWrapped ex =
exn("Oops", ex)
|> raise
前の例と同じ方法 ( を置換raise ex
) で適用すると、次のようなスタック トレースが得られます。特に、例外のルートであるcalc
関数が中間のどこかにあることに注意してください (ここではまだ明らかですが、複数のネストされた例外を含む深いトレースでは、それほど多くはありません)。
また、これはネストされた例外を尊重するトレース ダンプであることに注意してください。デバッグ中は、ネストされたすべての例外をクリックして確認する必要があります (最初からネストされていることに気付きます)。
System.Exception : Oops
----> System.DivideByZeroException : Attempted to divide by zero.
at Microsoft.FSharp.Core.Operators.Raise[T](Exception exn)
at Playful.Ex.TestRes.testme() in R:\path\Ex.fs:line 146
at Playful.Ex.Tests.TryItOut() in R:\path\Ex.fs:line 153
--DivideByZeroException
at Playful.Ex.Extern.calc(Int32 x, Int32 y) in R:\path\Ex.fs:line 118
at Playful.Ex.TestRes.testme@137-1.Invoke(Unit unitVar) in R:\path\Ex.fs:line 137
at Playful.Ex.Result.ResultBuilder.Run[b](FSharpFunc`2 f) in R:\path\Ex.fs:line 103
at Playful.Ex.Result.ResultBuilder.TryWith[a](FSharpFunc`2 body, FSharpFunc`2 handler) in R:\path\Ex.fs:line 105
結論
あるアプローチが別のアプローチよりも優れていると言っているのではありません。私にとって、が新しく作成され、以前に発生していない例外でraise ex
ない限り、無意識に行うことは良い考えではありません。ex
美しさは、上記reraise()
と同じことを効果的に行うことEx.throwPreserve
です。したがって、reraise()
(またはthrow
C# では引数なしで) が適切なプログラミング パターンであると思われる場合は、それを使用できます。reraise()
との唯一の違いEx.throwPreserve
は、後者はコンテキストを必要としないということですcatch
。これは、ユーザビリティの大幅な向上であると私は信じています。
結局、これは好みと慣れの問題だと思います。私にとっては、例外の原因を一番上に目立つようにしたいだけです。最初のコメンターであるTeaDrivenDevに感謝します.NET 4.5 の機能強化に私を誘導し、それ自体が上記の 2 番目のアプローチにつながりました。
(私自身の質問に答えて申し訳ありませんが、コメント者の誰もそれをしなかったので、私はステップアップすることにしました;)