代码之家  ›  专栏  ›  技术社区  ›  knocte

F#:在异步{}块内正确的`try`块?

  •  1
  • knocte  · 技术社区  · 5 年前

    为了简化我的场景,让我们假设我有以下简单的代码:

    let someCondition = false
    
    let SomeFuncThatThrows () =
        async {
            if someCondition then
                raise <| InvalidOperationException()
            return 0
        }
    
    let DoSomethingWithFoo (foo: int) =
        Console.WriteLine (foo.ToString())
    
    let SomeWrapper () =
        async {
            let! foo = SomeFuncThatThrows()
            DoSomethingWithFoo foo
        }
    
    [<EntryPoint>]
    let main argv =
        Async.RunSynchronously (SomeWrapper ())
        0
    

    执行它时,它显然只打印“0”。然而,总有一天,情况会发生变化,一些外部因素会使 someCondition 成为 true 。为了防止程序在这种情况下崩溃,我想处理异常。然后,对于F#新手来说,很容易更改SomeWrapper,添加一个try with block,大多数人会认为这是有效的:

    let SomeWrapper () =
        async {
            let! foo =
                try
                    SomeFuncThatThrows()
                with
                | :? InvalidOperationException ->
                    Console.Error.WriteLine "aborted"
                    Environment.Exit 1
                    failwith "unreachable"
            DoSomethingWithFoo foo
        }
    

    但是,上述操作不起作用(异常仍未处理),因为SomeFunThatThrows返回了一个成功的结果: Async<int> 元素。引发异常的是 let! foo = 位,因为它等待异步工作负载。

    但是,如果你想更改SomeWrapper来修复异常处理,许多人可能会认为这是可能的:

    let SomeWrapper () =
        async {
            let foo =
                try
                    let! fooAux = SomeFuncThatThrows()
                    fooAux
                with
                | :? InvalidOperationException ->
                    Console.Error.WriteLine "aborted"
                    Environment.Exit 1
                    failwith "unreachable"
            DoSomethingWithFoo foo
        }
    

    但是不,编译器不高兴,因为它发出以下错误信号:

    /.../Program.fs(17,17):错误FS0750:此构造只能使用 在计算表达式(FS0750)内(SomeProject)

    那么,似乎我唯一能解决这个问题的方法就是这样:

    let SomeWrapper () =
        async {
            try
                let! foo = SomeFuncThatThrows()
                DoSomethingWithFoo foo
            with
            | :? InvalidOperationException ->
                Console.Error.WriteLine "aborted"
                Environment.Exit 1
                failwith "unreachable"
        }
    

    然而,我对这个解决方案并不是100%满意,因为try-wwith太宽了,因为它还涵盖了对DoSomethingWithFoo函数的调用,我想把它放在try-wwith块之外。在不编写非惯用F#的情况下,有什么更好的方法来解决这个问题吗?我应该在微软的F#GitHub仓库中将编译器错误作为功能请求报告吗?

    0 回复  |  直到 5 年前
        1
  •  2
  •   knocte    5 年前

    您可以将通话转接至 SomeFuncThatThrows 在一个新的 async 包含a try...with :

    let SomeWrapper () =
        async {
            let! foo = 
                async {
                    try
                        return! SomeFuncThatThrows()
                    with
                    | :? InvalidOperationException ->
                        Console.Error.WriteLine "aborted"
                        Environment.Exit 1
                        return failwith "unreachable"
                }
            DoSomethingWithFoo foo
        }
    
        2
  •  1
  •   knocte    5 年前

    @nilekirk的答案是有效的,并直接编码了您要查找的逻辑,但正如您在评论中所指出的,这是一个相当复杂的句法结构——您需要一个嵌套 async { .. } 表达。

    你可以提取嵌套 async 将块转换为单独的函数,这使得代码更具可读性:

    let SafeSomeFunc () = async {
        try
            return! SomeFuncThatThrows()
        with
        | :? InvalidOperationException ->
            Console.Error.WriteLine "aborted"
            Environment.Exit 1
            return failwith "unreachable"
    }
    
    let SomeWrapper2 () = async {
        let! foo = SafeSomeFunc ()            
        DoSomethingWithFoo foo
    }
    

    在这里,我们实际上需要将一些返回值放入 with 支。

        3
  •  0
  •   Nghia Bui    5 年前

    在不编写非惯用F#的情况下,有什么更好的方法来解决这个问题吗?

    在习惯性的F#和函数式代码中,我们尽量避免使用异常和副作用。

    Environment.Exit 这是一个很大的副作用,不要使用它。

    如果 SomeFuncThatThrows() 必须能够抛出异常(因为例如,您无法修改其源代码)。然后尝试将其封装在一个安全函数中,该函数返回一个 Option 值并使用此函数。

    你的整个代码可以重写为:

    let someCondition = true
    
    let SomeFuncThatThrows () =
        async {
            if someCondition then
                raise <| InvalidOperationException()
            return 0
        }
    
    let SomeFunc () =
        async {
            try
                let! foo = SomeFuncThatThrows()
                return Some foo
            with _ ->
                return None
        }
    
    let DoSomethingWithFoo (foo: int) =
        Console.WriteLine (foo.ToString())
    
    let SomeWrapper () =
        async {
            match! SomeFunc() with
            | Some foo -> DoSomethingWithFoo foo
            | None -> Console.Error.WriteLine "aborted"
        }
    
    [<EntryPoint>]
    let main argv =
        Async.RunSynchronously (SomeWrapper ())
        0