Task EventHandlers: 一种避免async void的替代方案

在长时间的调试会话中,应用程序的功能有时会神秘地停止工作,这可能会让人感到困惑。处理异步事件处理器有多种不同的解决方案,无论是避免使用async void,还是接受async void,本文将探索另一种选择,即Task EventHandler。

本文最初是基于这样的观点撰写的:这种解决方案感觉接近无懈可击,但存在一些重要的限制。文章稍后将讨论这些限制,并且认为探索这个领域(包括优点和缺点)仍然很有价值。

首先,来探讨问题的根源。在C#中,正常的EventHandler具有void返回类型,这在需要连接一个标记为async的EventHandler时就成为了问题,因为希望等待内部调用的Task。

为什么这是一个问题?因为async void破坏了异常正常冒泡的能力,当无法追踪异常流向何处时,这可能会造成调试噩梦。根本的复杂性在于EventHandler的签名具有void返回类型,这破坏了异常控制:

void TheObject_TheEvent(object sender, EventArgs e);

但如果能够绕过这个问题呢?

Task EventHandlers可能有效!即将深入探讨的特定解决方案,其使用案例比之前提到的一些解决方案更为有限。然而,仍然认为,当能够控制添加到类中的事件,并且理解限制时,这是一个非常可行的解决方案。也就是说,如果正在创建一个类并定义自己的事件,希望调用者能够订阅,这个解决方案可能适合。将在后面讨论另一个重要的缺点,并且像所有事情一样,认为在做出决策之前,理解利弊是很重要的。

基于上一节所说的,可以尝试解决这里的问题,即EventHandler的void返回类型。当创建自己的事件时,通常会使用现有的委托签名来声明它们:

public event EventHandler<SomeEventArgs> MyEvent;

但是,这个EventHandler签名具有void返回类型。如果创建自己的呢?

public delegate Task AsyncEventHandler<TArgs>(object sender, TArgs args) where TArgs : EventArgs;

可以在GitHub上找到使用这个的示例,或者在下面:

public sealed class AsyncEventRaisingObject { public event AsyncEventHandler<EventArgs> ExplicitAsyncEvent; public async Task RaiseAsync(EventArgs e) { await ExplicitAsyncEvent?.Invoke(this, e); } }

现在让看一个示例应用程序,它结合了创建的委托以及上面定义的类。也可以在GitHub上找到这段代码:

Console.WriteLine("Starting the code example..."); var asyncEventRaisingObject = new AsyncEventRaisingObject(); asyncEventRaisingObject.ExplicitAsyncEvent += async (s, e) => { Console.WriteLine("Starting the event handler..."); await TaskThatThrowsAsync(); Console.WriteLine("Event handler completed."); }; try { Console.WriteLine("Raising our async event..."); await asyncEventRaisingObject.RaiseAsync(EventArgs.Empty); } catch (Exception ex) { Console.WriteLine($"Our exception handler caught: {ex}"); } Console.WriteLine("Completed the code example."); async Task TaskThatThrowsAsync() { Console.WriteLine("Starting task that throws async..."); throw new InvalidOperationException("This is our exception"); }

上述代码将为设置一个Task EventHandler,它最终会因为等待的Task而抛出异常。由于定义的事件签名是Task而不是void,这使能够有一个Task EventHandler。可以在下面看到结果:

有一个大问题,这个实现对于许多用例来说可能是一个破坏者。然而,根据想要实现的目标,可能会有一些创造性的解决方案。

事件和EventHandler并不完全像回调那样运作。从它们那里得到的+/-语法允许向一个调用列表添加和移除处理程序。Task EventHandler在早期执行的处理程序抛出异常,而有一个后续的处理程序时会崩溃。如果颠倒顺序,抛出异常的Task EventHandler位于调用的末尾,将得到在上一节中展示的行为。鉴于这种行为对于事件订阅者来说可能感觉非常不一致,这让陷入了困境。

虽然不一定建议这样做,但认为根据用例,可能会考虑以下情景。如果设计只要求对象生命周期内有一个处理程序,可以添加自定义的添加/移除事件语法。也就是说,在添加重载期间,可以检查是否为null,并且只在这种情况下允许注册。

另一种可能的替代方案是探索以下内容:

public event AsyncEventHandler<EventArgs> ExplicitAsyncEvent { add { _explicitAsyncEvent += async (s, e) => { try { await value(s, e); } catch (Exception ex) { // TODO: do something with this exception? await Task.FromException(ex); } }; } remove { _ExplicitAsyncEvent -= value; } }

在上面的代码中,实际上使用了这篇文章中的技巧,将处理程序包装在try/catch中。通过这种方式分层try/catch会创建其他复杂性,关于打算如何处理异常,以及取消挂钩事件也会变得更加复杂。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485