我们可以有一个异步动作吗?

Can we have an async Action?

我制作了一个运行良好的事件系统,允许我通过传递事件类型和处理程序来注册事件处理程序,这是一个操作,例如

EventManager.RegisterEventHandler<Events.NewCompleteSample>(HandleCompleteSample);

打电话

public static void RegisterEventHandler<T>(Action<T> handlerAction)

稍后引发事件时,我调用已注册的操作。

我的问题是我随机发生无声崩溃,我现在意识到这可能是因为我调用的操作可能(经常是)异步方法,但为时已晚我才知道非等待异步方法中的异常是没有被抓到任何地方。

解决方案似乎是等待像

这样的处理程序
await handler.Invoke(arguments)

但是因为我不知道有什么方法可以指定 Actions 是异步方法,所以这行不通。

有没有办法将一个 Action 定义为异步的,这样您可以在以后调用它时等待它,或者可能有更聪明的解决方案来解决这个问题?

您可以使用 Func 创建一个“可等待的动作”,它接收一个对象类型的参数并且没有 return 类型。 https://docs.microsoft.com/en-us/dotnet/api/system.func-2?f1url=%3FappId%3DDev16IDEF1%26l%3DEN-US%26k%3Dk(System.Func%602);k(SolutionItemsProject);k(DevLang-csharp)%26rd%3Dtrue&view=net-6.0

    private async Task ExecuteAsync(Func<object, Task> action, object args)
    {
       await action.Invoke(args);
    }

My problem is that I randomly have silent crashes, and I now realize that this might be because the actions I call may (frequently are) async methods, and too late I am learning that exceptions in non awaited async methods are not caught anywhere.

有点。 normal 异步方法抛出的异常(即 return TaskTask<T> 的方法)如果从未观察到任务,则将被忽略。 Exceptions thrown from async void methods do crash the process;这是一个深思熟虑的设计决定。

Is there a way to define an Action as async,

当然;只需将委托映射到函数签名,使它们异步,然后再映射回来。这将为您提供 asynchronous delegate types(如我的博客所述)。

在这种情况下,Action 映射到类似 void Handle() 的方法;其异步版本是 Task HandleAsync(),它映射到 Func<Task>.

类型的委托

so you can await it when later calling it,

好吧,有点。当然,await handler.Invoke(); 会编译,但是事件(和委托)可以容纳任意数量的处理程序,而 Invoke 只是 return 的 最后一个 结果,所以 await 等待来自最后一个处理程序的 Task returned。如果有多个处理程序,所有其他 Tasks 将被丢弃(并且它们的异常会被默默地吞下)。

or might there be a smarter solution to this issue?

asynchronous events(我的博客link)有一些解决方案。

首先,我会考虑调整设计。事件非常适合观察者设计模式,但需要 await 处理程序通常意味着事件被滥用来实现不同的设计模式,通常是命令或策略设计模式。考虑用不同的东西完全替换事件,比如接口。

如果这不可行,那么我会考虑通过调查 Reactive Observables 使用不同的事件表示。 Observables 确实是事件 应该 的样子。但是 Observables 可能会变得非常复杂。

另一种方法是使用延迟。本质上,您保留了 async void 方法,但处理程序需要通过在 using 中获取延迟来“征募”异步处理。然后调用代码等待所有延迟被释放。

最后的方法是使用 Func<Task>,通过 GetInvocationList 获取 所有 任务,并且一次 await 它们一个或Task.WhenAll.