【问题标题】:Can I avoid duplicate code in similar methods for synchronous and async lambdas?我可以避免在同步和异步 lambda 的类似方法中重复代码吗?
【发布时间】:2016-05-02 21:35:29
【问题描述】:

我有一个辅助方法,它需要一个 lambda 在 try/catch/finally 块中执行,例如

public void ProcessSpreadsheet(string filename, Action<object[,]> process) {
    try {
        // Open MS Excel workbook
        // Open sheet & extract data into valueArray
        // ... more boiler plate ...

        process(valueArray);
    }
    catch (FooException e) {
        LogFoo(e.Message);
        throw;
    }
    catch (BarException e) {
        LogBar(e.Message);
        throw;
    }
    finally {
        // Close workbook, release resources, etc..
    }
}

我现在想创建一个接受异步 lambda 的异步版本。

(请注意,我已阅读并同意 Stephen Toub 的 posts 关于异步优于同步,但这不适用于该方法的症结所在,其中异步是或无益,由消费者通过 lambda 提供。)

我可以复制粘贴上面的内容,将其标记为async,将返回类型更改为Task,在其名称中添加“Async”,将process参数的类型更改为Func&lt;object[,],Task&gt;并更改

process(valueArray);

await process(valueArray);

一切正常。但我想避免重复 try/catch/finally 中的所有代码。有没有一种简洁的方法可以在不重复代码的情况下实现这一目标?

到目前为止,我得到的最佳解决方案是:

public async Task ProcessSpreadsheetAsync(string filename, Func<object[,],Task> process) {
    var asyncTaskComplete = new ManualResetEvent(false);
    ProcessSpreadsheet(filename, async valueArray => {
        await process(valueArray);
        asyncTaskComplete.Set();
    });
    await Task.Run(() => asyncTaskComplete.WaitOne());
}

但它很混乱,并且没有处理异常。我想知道是否还有其他方法?

【问题讨论】:

  • 您要避免重复哪些代码? try/catch/finally 代码?为什么在异步版本中通过Task.Run 运行代码?你到底想用异步版本实现什么?
  • @YacoubMassad 是的,应该是 Func,我已经解决了,谢谢。
  • 这段代码真的有效吗? process 抛出的异常会被 catch 块捕获吗?
  • 是的,我想避免重复 try/catch/finally 代码。对于传入的 lambda 将从异步中受益的情况,我想要一个异步版本。例如,我使用同步版本从电子表格中计算总数,但我想要一个异步版本,在该版本中我对电子表格中的每一行进行网络调用。这会很慢,而且我想保持 UI 响应,所以我想在异步 lambda 中进行。
  • @YacoubMassad,不,你又是对的,没有发现异常:(。我想我需要的帮助比我想象的要多。

标签: c# asynchronous lambda async-await


【解决方案1】:

要回答您的实际问题而不是告诉您做其他事情,只需将 try/catch 块提取到函数中即可。通常建议将其作为关注点分离的一部分。带有错误处理的乱扔逻辑是不好的形式。

【讨论】:

    【解决方案2】:

    我会放弃接受 lambda 的想法。这是在您的代码中接受真正不属于的责任。相反,只需提供一个如下所示的重载:

    public object[,] ProcessSpreadsheet(string filename) {
        try {
            // Open MS Excel workbook
            // Open sheet & extract data into valueArray
            // ... more boiler plate ...
    
            return valueArray;
        }
        catch (FooException e) {
            LogFoo(e.Message);
            throw;
        }
        catch (BarException e) {
            LogBar(e.Message);
            throw;
        }
        finally {
            // Close workbook, release resources, etc..
        }
    }
    

    如果同步代码正在使用它,它只会这样做:

    try {
        var results = ProcessSpreadsheet("...");
        DoSomethingElse(results);
    } catch (FooException e) {
        // ...
    } catch (CanHappenWhileDoingSomethingElseException e) {
        // ...
    }
    

    就处理异常而言,代码的使用者具有无限的灵活性。

    如果有人在使用 async/await,他们所要做的就是:

    try {
        var results = await Task.Run(() => ProcessSpreadsheet("..."));
        DoSomethingElse(results);
    } catch (FooException e) {
        // ...
    } catch (CanHappenWhileDoingSomethingElseException e) {
        // ...
    }
    

    他们所有的错误处理方式都完全相同。

    【讨论】:

      【解决方案3】:

      我已阅读并同意 Stephen Toub 关于异步优于同步的帖子,但这不适用于该方法的症结所在,异步是否有益,由消费者通过 lambda 提供。

      嗯,是的,也不是。我可以看到您所说的消费代码将确定它是否是异步的。但为避免重复,您仍需注意同步异步和异步同步反模式。

      在解决方案方面,首先想到的是接受异步 lambda。这在概念上类似于在接口中定义 Task-returning 方法 - 实现可能是异步的,也可能是同步的。

      在你的情况下,它看起来像:

      public async Task ProcessSpreadsheetAsync(string filename, Func<object[,],Task> process) {
        try {
          // Open MS Excel workbook
          // Open sheet & extract data into valueArray
          // ... more boiler plate ...
      
          await process(valueArray);
        }
        catch (FooException e) {
          LogFoo(e.Message);
          throw;
        }
        catch (BarException e) {
          LogBar(e.Message);
          throw;
        }
        finally {
          // Close workbook, release resources, etc..
        }
      }
      

      我会把它留在那里。任何同步进程都可以:

      await ProcessSpreadsheetAsync(filename, data => { ...; return Task.FromResult(true); });
      

      在这种特殊情况下,您也可以像这样编写包装器:

      public void ProcessSpreadsheet(string filename, Action<object[,]> process)
      {
        ProcessSpreadsheetAsync(filename, async data => { process(data); }).GetAwaiter().GetResult();
      }
      

      但是,这是唯一安全的,因为ProcessSpreadsheetAsync 只有一个await,它在其process 上。如果将ProcessSpreadsheetAsync 更改为另一个await,则包装器很容易导致死锁。

      【讨论】:

      • 在这里复制代码而不是强制同步方法运行 async 不是更好吗?因为它带来了堆上额外分配的额外成本。
      • @NazmiAltun:同步方法仍然同步运行;他们返回一个已完成的任务。如果要避免分配,可以为Task.FromResult(true) 提供static 字段或使用ValueTask
      猜你喜欢
      • 1970-01-01
      • 2014-07-18
      • 1970-01-01
      • 1970-01-01
      • 2018-02-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-02-23
      相关资源
      最近更新 更多