Abe*_*bel 9 monads f# try-catch computation-expression
TL; DR:如何在以后引发先前捕获的异常,同时保留原始异常的堆栈跟踪.
因为我认为这对Resultmonad或计算表达式很有用,尤其是 因为该模式通常用于包装异常而不抛出异常,所以这里有一个很好的例子:
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()
Run Code Online (Sandbox Code Playgroud)
现在让我们使用它:
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
Run Code Online (Sandbox Code Playgroud)
问题是堆栈跟踪不包含异常的来源(这里是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
Run Code Online (Sandbox Code Playgroud)
使用reraise()不起作用,它需要一个catch-context.显然,以下类型 - 一个工作,但由于嵌套异常使调试更难,如果这个wrap-reraise-wrap-reraise模式在深层堆栈中被多次调用,可能会变得非常难看.
System.Exception("Oops", ex)
|> raise
Run Code Online (Sandbox Code Playgroud)
更新:TeaDrivenDev在评论中建议使用ExceptionDispatchInfo.Capture(ex).Throw(),这有效,但需要将异常包装在其他内容中,使模型复杂化.但是,它确实保留了堆栈跟踪,它可以成为一个相当可行的解决方案.
我害怕的一件事是,一旦你将异常视为普通对象并传递它,你将无法再次提升它并保持其原始的堆栈跟踪.
但是,如果你这样做,中间或最后都是如此raise excn.
我已经从评论中获取了所有想法,并在此将它们显示为问题的三个解决方案.选择最适合自己的方式.
以下示例显示了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."
Run Code Online (Sandbox Code Playgroud)
使用原始问题(替换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
Run Code Online (Sandbox Code Playgroud)
如果您没有.NET 4.5,或者不喜欢跟踪中间添加的行("---从抛出异常的上一个位置的堆栈跟踪结束---"),那么您可以保留堆栈并一次添加当前跟踪.
我通过遵循TeaDrivenDev的解决方案找到了这个解决方案,并在重新抛出异常时发生了保留堆栈跟踪.
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
Run Code Online (Sandbox Code Playgroud)
通过原始问题(替换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
Run Code Online (Sandbox Code Playgroud)
这是由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
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
我不是说一种方法比另一种更好.对我来说,只是盲目地做raise ex不是一个好主意,除非ex是一个新创建的,而不是以前提出的异常.
美丽是reraise()有效地与Ex.throwPreserve上面做的一样.因此,如果你认为reraise()(或者throw没有C#中的参数)是一个很好的编程模式,你可以使用它.reraise()和之间的唯一区别Ex.throwPreserve是后者不需要catch上下文,我认为这是一个巨大的可用性增益.
我想最终这是一个品味和你习惯的问题.对我而言,我只是希望将例外的原因放在首位.非常感谢第一位评论者,TeaDrivenDev,他指导我进行.NET 4.5增强,这本身就是第二种方法.
(对于回答我自己的问题道歉,但由于没有一个评论者这样做,我决定加强;)