2012-01-19 81 views
3

我在调用一个恰好为null的嵌套异步时偶然发现了一个问题。引发了一个异常,但无法使用Async工作流提供的任何常规异常处理方法。意外的异步处理行为,异步,可能的错误?

下面是一个简单的测试,重新产生此问题:

[<Test>] 
let ``Nested async is null with try-with``() = 

    let g(): Async<unit> = Unchecked.defaultof<Async<unit>> 

    let f = async { 
      try 
       do! g() 
      with e -> 
       printf "%A" e 
    } 

    f |> Async.RunSynchronously |> ignore 

这导致follwing例外:

System.NullReferenceException : Object reference not set to an instance of an object. 
at [email protected](AsyncParams`1 args) 
at <StartupCode$FSharp-Core>[email protected](Trampoline this, FSharpFunc`2 action) 
at Microsoft.FSharp.Control.Trampoline.ExecuteAction(FSharpFunc`2 firstAction) 
at Microsoft.FSharp.Control.TrampolineHolder.Protect(FSharpFunc`2 firstAction) 
at Microsoft.FSharp.Control.AsyncBuilderImpl.startAsync(CancellationToken cancellationToken,  FSharpFunc`2 cont, FSharpFunc`2 econt, FSharpFunc`2 ccont, FSharpAsync`1 p) 
at [email protected]oke(CancellationToken  cancellationToken, FSharpFunc`2 cont, FSharpFunc`2 econt, FSharpFunc`2 ccont, FSharpAsync`1 p) 
at Microsoft.FSharp.Control.CancellationTokenOps.RunSynchronously(CancellationToken token, FSharpAsync`1 computation, FSharpOption`1 timeout) 
at Microsoft.FSharp.Control.FSharpAsync.RunSynchronously(FSharpAsync`1 computation, FSharpOption`1 timeout, FSharpOption`1 cancellationToken) 
at Prioinfo.Urkund.DocCheck3.Core2.Tests.AsyncTests.Nested async is null with try-with() in SystemTests.fs: line 345 

我真的觉得异常应在这种情况下被抓,或者是这真的是预期的行为? (我使用Visual Studio 2010 SP1备案)

此外,Async.CatchAsync.StartWithContinuations展品同样的问题,这表现在这些测试用例:

[<Test>] 
let ``Nested async is null with Async.Catch``() = 

    let g(): Async<unit> = Unchecked.defaultof<Async<unit>> 

    let f = async { 
       do! g() 
      } 

    f |> Async.Catch |> Async.RunSynchronously |> ignore 


[<Test>] 
let ``Nested async is null with StartWithContinuations``() = 

    let g(): Async<unit> = Unchecked.defaultof<Async<unit>> 

    let f = async { 
       do! g() 
      } 

    Async.StartWithContinuations(f 
           , fun _ ->() 
           , fun e -> printfn "%A" e 
           , fun _ ->()) 

看来唯一的例外是bind-内提出方法在工作流生成器中,我的猜测是结果是正常的错误处理代码被绕过。它看起来像是在执行异步工作流程时遇到的一个错误,因为我没有在文档或其他地方发现任何暗示这是预期行为的东西。

在大多数情况下,我很容易解决这个问题,至少对我来说这不是一个大问题,但这有点令人不安,因为这意味着您不能完全信任异步异常处理机制捕捉所有的例外。

编辑:

一番考虑我KVB同意后。空的asyncs不应该真正存在于普通代码中,只有当你做了你可能不应该做的事情时(例如使用Unchecked.defaultOf)或使用反射来产生值(在我的情况下,它是一个模拟的框架) 。因此它不是一个真正的bug,而更像是一个边缘案例。

+0

我可以重现它在FSI和控制台应用程序,无论是在VS调试,并从资源管理器启动时的时候。所以它看起来是一致的 –

+0

Option.None基本上是空的。所以看起来像Async <'a option>可以有这个问题。 – Hans

回答

4

我不认为这是一个错误。由于名称表示Unchecked.defaultof<_>未检查其生成的值是否有效,并且Async<unit>不支持null作为适当的值(例如,如果尝试使用let x : Async<unit> = null,请参阅消息)。 Async.Catch等旨在捕获在异步计算内抛出的异常,而不是由于潜入编译器背后造成的异常以及创建无效的异步计算。

+0

我可以看到你的观点,但另一方面,语法的种类使你期望应该抓住例外。如果你在一个正常的try-catch块中调用一个空的委托,那么这个异常就会被捕获。 –

+0

@SamuelOtter - 一般来说,使用'Unchecked.defaultof <_>'时,经常会导致问题,当您使用它为不具有空值的类型创建空值作为有效表示时。请注意,对于委托,null是一个完全可接受的值(例如,编译器对'let x:System.Action = null'没有问题)。 – kvb

+0

我知道,Unchecked.defaultOf只在测试中用来激发测试用例的空值。我同意在实践中空异步应该是罕见的,但它毕竟可能发生。我最初发现这是因为我有一个异步方法的模拟对象,但没有任何期望,模拟框架的默认行为是简单地为所有方法返回null。但我同意将它称为一个错误可能有点强,它更像是一个无证的边缘案例。 –

4

我完全同意kvb - 当您使用Unchecked.defaultOf初始化一个值时,这意味着使用该值的行为可能是未定义的,所以这不能被视为错误。在实践中,你不必担心它,因为你永远不应该得到Async<'T>类型的值null

要添加一些更多的细节,该异常不能被处理,因为翻译如下所示:

async.TryWith 
    (async.Bind (Unchecked.defaultof<_>, 
       fun v -> async { printfn "continued" }), 
    fun e -> printfn "%A" e) 

唯一的例外是从Bind方法之前抛出由Bind返回的工作流程开始(它在调用RunSynchronously之后发生,因为工作流程使用Delay进行封装,但发生在工作流程执行之外)。如果你要处理这种类型的异常(不正确地构建工作流所产生的),你可以写一个版本的TryWith运行的工作流程和处理异常的执行外抛出:

let TryWith(work, handler) = 
    Async.FromContinuations(fun (cont, econt, ccont) -> 
    try 
     async { let! res = work in cont res } 
     |> Async.StartImmediate 
    with e -> 
     async { let! res = handler e in cont res } 
     |> Async.StartImmediate) 

然后你就可以处理诸如异常这样的:

let g(): Async<unit> = Unchecked.defaultof<Async<unit>> 
let f = 
    TryWith 
     ((async { do! g() }), 
     (fun e -> async { printfn "error %A" e })) 
f |> Async.RunSynchronously 
+0

尽管我同意这不是一个真正的bug,但我不同意它在实践中永远不会发生,因为它确实发生在我身上。在我的情况下,它是在一个单元测试中由一个模拟框架引起的,所以它没有太大关系(即使我花了一天的时间相信我的代码在意识到问题出现之前就被破坏了)。在惯用的F#代码中,这种情况不应该发生,但我可以想象有人会因为这种情况被例如使用DI容器创建或注入Async值或类似的东西而被咬伤。 –