在F#中处理资源清理的功能性方法

4

您好,这是一个关于C#语言如何处理Com interop资源管理的示例。 原始来源

Excel.Application app = null;
Excel.Workbooks books = null;
Excel.Workbook book = null;
Excel.Sheets sheets = null;
Excel.Worksheet sheet = null;
Excel.Range range = null;

try
{
    app = new Excel.Application();
    books = app.Workbooks;
    book = books.Add();
    sheets = book.Sheets;
    sheet = sheets.Add();
    range = sheet.Range["A1"];
    range.Value = "Lorem Ipsum";
    book.SaveAs(@"C:\Temp\ExcelBook" + DateTime.Now.Millisecond + ".xlsx");
    book.Close();
    app.Quit();
}
finally
{
    if (range != null) Marshal.ReleaseComObject(range);
    if (sheet != null) Marshal.ReleaseComObject(sheet);
    if (sheets != null) Marshal.ReleaseComObject(sheets);
    if (book != null) Marshal.ReleaseComObject(book);
    if (books != null) Marshal.ReleaseComObject(books);
    if (app != null) Marshal.ReleaseComObject(app);
} 

我认为上面的代码是合理和必要的,但它不够实用或符合F#的方式。我最终在不同层次的try...finally和try...with中定义了所有这些com变量,并且由于变量必须在try块之前定义,因此清理代码存在于finally和with块中。这非常混乱。

如何在F#中正确地实现相同的功能?有点讽刺的是,互操作性在网上有很多示例,作为展示F#功能的一种方式。然而,它们都没有涉及如何管理com资源清理。

任何关于良好模式的建议都将不胜感激。


3
简单介绍一下,因为我在手机上:使用IDisposable对象(如果Com没有实现IDisposable,则使用包装器),并使用F#中的use表达式。它类似于let,但在对象超出范围时调用Dispose()。 - rmunn
1个回答

6

您可以创建一个计算表达式,其中每个步骤都在try/catch中调用,并在完成时在finally中释放。我们可以创建一个带有创建/完成插件函数的构建器,以便我们可以看到正在发生的情况。

type FinalizationBuilder(oncreate, onfinal) =
  member __.Bind(m, f) = 
    oncreate(box m)
    try
      try
        f m
      with ex ->
        Choice2Of2 ex.Message
    finally
      onfinal(box m)
  member __.Return(m) = Choice1Of2 m
  member __.Zero() = Choice1Of2()

那么你需要一个COM工作流,它会在完成时释放COM组件。

let com = new FinalizationBuilder(ignore, System.Runtime.InteropServices.Marshal.ReleaseComObject >> ignore)

您可以像这样使用它:
[<EntryPoint>]
let main _ = 
  com { 
    let! app = new Excel.Application()
    let! books = app.Workbooks
    let! book = books.Add()
    // ...
    app.Quit()
  } |> ignore
  0

我没有安装Excel,但是我可以通过异常和printfns来模拟它。

let demo = new FinalizationBuilder(printfn "Created %A", printfn "Released %A")

[<EntryPoint>]
let main _ = 
  demo { 
    let! x = 1
    let! y = 2
    let! z = 3
    return x + y + z
  } |> printfn "Result: %A"
  0

// Created 1
// Created 2
// Created 3
// Released 3
// Released 2
// Released 1
// Result: Choice1Of2 6

或者有异常情况:
[<EntryPoint>]
let main _ = 
  demo { 
    let! x = 1
    let! y = 2
    let! z = failwith "boom"
    return x + y + z
  } |> printfn "Result: %A"
  0

// Created 1
// Created 2
// Released 2
// Released 1
// Result: Choice2Of2 "boom"

所有这些说法似乎都是不必要的。一个简单的GC.Collect(); GC.WaitForPendingFinalizers()可以解决问题,而无需任何其他操作:https://dev59.com/xV8e5IYBdhLWcg3w_-r0#25135685


嗨Dax,我现在无法测试代码,但我可以问一下let!z = failwith“boom”代码失败是在创建z时而不是执行x + y + z时发生的。这意味着如果程序将使用interop excel对象执行某些操作,则逻辑的每一行都必须使用let绑定编写,对吗?如果在x + y + z中发生异常并且没有包装在let绑定中,则错误处理和释放逻辑将不适用? - casbby
1
@casbby 不是的,计算表达式处理其中发生的任何异常,并应用释放逻辑。因此,如果您将第一个示例更改为return x + y + z + (failwith“boom”),它将释放到目前为止创建的所有内容(x,y,z),并返回Choice2Of2“boom” - Dax Fohl
真的!它为每个let级别有效地创建了一个嵌套的try ..with ...finally!这样我就不必手动定义它们了。非常感谢如此快速的回复。还有一个问题。我不完全理解zero()。它的一般用途和在本例中的目的是什么?您的代码将在指定zero()时与否都能正常工作。 - casbby
2
如果表达式中没有return,那么这是必要的。比如在第一个例子中,表达式以app.Quit()结尾而没有return - Dax Fohl

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接