2012-01-19 76 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值或類似的東西而被咬傷。 –