TL; DR:如何在以后保留原有异常的堆栈跟踪的同时引发先前捕获的异常。

由于我认为这对Result monad或计算表达式特别有用。由于该模式通常用于包装异常而不抛出异常,因此下面是一个经过实践的示例:

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可行,但由于嵌套的异常而使调试更加困难,并且如果在深层堆栈中多次调用该wrap-reraise-wrap-reraise模式,可能会变得非常难看。
System.Exception("Oops", ex)
|> raise

更新:TeaDrivenDev在注释中建议使用ExceptionDispatchInfo.Capture(ex).Throw(),它可以工作,但需要将异常包装在其他内容中,从而使模型复杂化。但是,它确实保留了堆栈跟踪,并且可以做成一个相当可行的解决方案。

最佳答案

我担心的一件事是,一旦将异常视为普通对象并将其传递,您将无法再次引发它并保留其原始堆栈跟踪。

但这只有在您在raise excn中间或结尾使用ExceptionDispatchInfo.Capture的情况下才是正确的。

我已经从注释中吸收了所有想法,并在此处将其作为解决问题的三种解决方案。选择对您来说最自然的选择。

使用ExceptionDispatchInfo捕获堆栈跟踪

以下示例使用raise ex展示了TeaDrivenDev的建议。

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

完全保留stacktrace

如果您没有.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

在原始问题中的示例(替换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提出的,可能是.NET的默认方式,因为它在BCL中经常使用。但是,它会导致在许多情况下较少使用的堆栈跟踪,并且imo可能导致在深层嵌套的函数中混淆困惑的跟踪。
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

以与前面的示例相同的方式应用(替换calc),这将为您提供如下的堆栈跟踪。特别要注意的是,异常的根源raise ex函数现在位于中间的某个位置(在这里仍然很明显,但是在具有多个嵌套异常的深层跟踪中就不再如此)。

还要注意,这是一个跟踪转储,它遵循嵌套的异常。调试时,需要单击所有嵌套的异常(并意识到它是嵌套的)。

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

结论

我并不是说一种方法比另一种更好。对我来说,只是盲目地执行ex并不是一个好主意,除非reraise()是一个新创建的并且以前没有引发过异常。

这样做的好处是Ex.throwPreserve的作用与上述reraise()的作用相同。因此,如果您认为throw(或C#中不带参数的reraise())是一种不错的编程模式,则可以使用它。 Ex.throwPreservecatch之间的唯一区别是后者不需要ojit_code上下文,我认为这是巨大的可用性。

我想最后这是一个品味和习惯的问题。对我来说,我只想将异常原因放在首位。非常感谢第一位评论者TeaDrivenDev的指导,他将我定向到了.NET 4.5增强功能,该功能本身导致了上述第二种方法。

(为回答我自己的问题而道歉,但由于没有评论者这样做,因此我决定加强;)

关于f# - 当从catch上下文中抛出异常时,如何保持stacktrace?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/41193629/

10-11 22:37
查看更多