11

TL;DR: 元の例外のスタックトレースを保持しながら、以前にキャッチされた例外を後で発生させる方法。

Resultこれはモナドや計算式で便利だと思うので、特に。そのパターンは、例外をスローせずにラップするためによく使用されるため、その例を次に示します。

type Result<'TResult, 'TError> =
    | Success of 'TResult
    | Fail of 'TError

module Result =
    let bind f = 
        function
        | Success v -> f v
        | Fail e -> Fail e

    let create v = Success v

    let retnFrom v = v

    type ResultBuilder () =
        member __.Bind (m , f) = bind f m
        member __.Return (v) = create v
        member __.ReturnFrom (v) = retnFrom v
        member __.Delay (f) = f
        member __.Run (f) = f()
        member __.TryWith (body, handler) =
            try __.Run body
            with e -> handler e

[<AutoOpen>]
module ResultBuilder =
    let result = Result.ResultBuilder()

そして今それを使用しましょう:

module Extern =
    let calc x y = x / y


module TestRes =
    let testme() =
        result {
            let (x, y) = 10, 0
            try
                return Extern.calc x y
            with e -> 
                return! Fail e
        }
        |> function
        | Success v -> v
        | Fail ex -> raise ex  // want to preserve original exn's stacktrace here

問題は、スタックトレースに例外のソース(ここではcalc関数) が含まれないことです。書かれているとおりにコードを実行すると、次のようにスローされ、エラーの原因に関する情報は得られません。

System.DivideByZeroException : Attempted to divide by zero.
   at Microsoft.FSharp.Core.Operators.Raise[T](Exception exn)
   at PlayFul.TestRes.testme() in D:\Experiments\Play.fs:line 197
   at PlayFul.Tests.TryItOut() in D:\Experiments\Play.fs:line 203

使用reraise()は機能しません。キャッチコンテキストが必要です。明らかに、次の種類の a は機能しますが、ネストされた例外のためにデバッグが難しくなり、このラップ-リレイズ-ラップ-リレイズ パターンがディープ スタックで複数回呼び出されると、かなり醜くなる可能性があります。

System.Exception("Oops", ex)
|> raise

更新: TeaDrivenDev は、コメントで使用することを提案しましたExceptionDispatchInfo.Capture(ex).Throw()。これは機能しますが、例外を別のものでラップする必要があり、モデルが複雑になります。ただし、スタックトレースは保持され、かなり実行可能なソリューションにすることができます。

4

2 に答える 2

10

私が恐れていたことの 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()(またはthrowC# では引数なしで) が適切なプログラミング パターンであると思われる場合は、それを使用できます。reraise()との唯一の違いEx.throwPreserveは、後者はコンテキストを必要としないということですcatch。これは、ユーザビリティの大幅な向上であると私は信じています。

結局、これは好みと慣れの問題だと思います。私にとっては、例外の原因を一番上に目立つようにしたいだけです。最初のコメンターであるTeaDrivenDevに感謝します.NET 4.5 の機能強化に私を誘導し、それ自体が上記の 2 番目のアプローチにつながりました。

(私自身の質問に答えて申し訳ありませんが、コメント者の誰もそれをしなかったので、私はステップアップすることにしました;)

于 2016-12-17T19:21:12.013 に答える