如何在 F# 的结果中处理 IDisposable?

How do I handle IDisposable's inside a Result in F#?

我想 return 一个 HttpResponseMessage,它在 Result(或任何其他有区别的联合)中实现 IDisposable。但是,由于 Result 本身不是 IDisposable,我不能像对一次性对象本身那样 use! 它。我该怎么办?我可以实现自己的名为 DisposableResult 的 DU,它本身实现了 IDisposable 吗?

下面是我的意思的一个例子。 crawlStuff 异步 returns Result<HttpResponseMessage, string>。我无法 use! 结果,导致内存泄漏,除非我手动释放它。

open System.Net.Http

let crawlStuff (client : HttpClient) : Async<Result<HttpResponseMessage, string>> = 
    async {
        // res is HttpResponseMessage which implements IDisposable
        let! res = client.GetAsync("https://ifconfig.me") |> Async.AwaitTask
        return
            if res.IsSuccessStatusCode then
                Ok res
            else
                Error "Something wrong"
    } 

async {
    use client = new HttpClient()

    // Memory leak because result it could carry HttpResponseMessage.
    // I want to use! here, but can't because Result<> is not IDisposable
    let! response = crawlStuff client

    printfn "%A" response
} |> Async.RunSynchronously

是的,您可以实现自己的一次性结果类型,如下所示:

type DisposableResult<'t, 'terr when 't :> IDisposable> =
     | DOk of 't
     | DError of 'terr

     interface IDisposable with
          member this.Dispose() =
               match this with
                    | DOk value -> value.Dispose()
                    | _ -> ()

用法将是:

open System.Net.Http

let crawlStuff (client : HttpClient) : Async<Result<HttpResponseMessage, string>> = 
    async {
        // res is HttpResponseMessage which implements IDisposable
        let! res = client.GetAsync("https://ifconfig.me") |> Async.AwaitTask
        return
            if res.IsSuccessStatusCode then
                DOk res
            else
                DError "Something wrong"
    } 

async {
    use client = new HttpClient()
    use! response = crawlStuff client
    printfn "%A" response
} |> Async.RunSynchronously

我会围绕 Result 创建包装器,这将处理基础值:

let (|AsDisposable|) x =
    match box x with
    | :? IDisposable as dis -> ValueSome dis
    | _ -> ValueNone

type ResultDisposer<'v, 'e> =
    struct
        val Result : Result<'v, 'e>
        new res = { Result = res }
        interface IDisposable with
            member r.Dispose() =
                match r.Result with
                // | Ok (:? IDisposable as dis) // causes FS0008, so we have to hack
                | Ok (AsDisposable (ValueSome dis))
                | Error (AsDisposable (ValueSome dis)) -> dis.Dispose()
                | _ -> ()
    end

type Result<'a, 'b> with
    member r.AsDisposable = new ResultDisposer<'a, 'b>(r)

并这样使用

async {
    use client = new HttpClient()
    let! response = crawlStuff client
    use _ = response.AsDisposable

    printfn "%A" response
} |> Async.RunSynchronously

此解决方案避免了将现有代码重写为 DisposableResult 的需要,并避免了一次性值是引用类型时的分配,例如 HttpResponseMessage 的情况。但是反编译显示 F# 框 ResultDisposer,即使它不应该:(