paint-brush
异步事件处理程序的简单安全网by@devleader
750
750

异步事件处理程序的简单安全网

Dev Leader8m2023/02/14
Read on Terminal Reader

Async void 是我们似乎允许可怕的异步 EventHandlers 设置的唯一例外。在本文中,我将介绍另一种解决方案,您可以在自己的代码中进行尝试。我们将从我的角度讨论关于如何使用它的优缺点,以便您决定它是否有意义。
featured image - 异步事件处理程序的简单安全网
Dev Leader HackerNoon profile picture

当我们讨论异步事件处理程序时,我们很多人首先想到的是它是我们似乎允许的唯一例外可怕的 async void 设置.


当我之前写过这篇文章时,我很兴奋,因为我正在探索一种解决方案,该解决方案实际上允许 async void 存在(不想把我剩下的头发拉出来)。


对我来说,这更多是关于我们可以用来克服异步事件处理程序的一些巧妙技巧,而不是提供完全避免该问题的解决方案。


话虽如此,这篇文章引起了很多关注,对此我深表感谢,一些人表达了他们宁愿以不同方式解决异步事件处理程序的意见。


我认为这是一个很好的观点,所以我想提出一种替代方法,它不会修复 async void,但它允许你在解决一些挑战的同时完全避免它(看看我在那里做了什么?)使用异步事件处理程序。


在本文中,我将介绍另一种解决方案,您可以在自己的代码中进行尝试。我们将从我的角度阐述关于如何使用它的利弊,以便您可以决定它是否对您的用例有意义。


您还可以在 .NET fiddle 上找到一些可交互的代码在这里.否则,你可以在 GitHub 上查看代码如果您想将其克隆到本地进行试用。

配套视频!

点击这里查看视频!

问题

我们面临的异步事件处理程序问题是,我们可以在 C# 中默认订阅的事件签名如下所示:


 void TheObject_TheEvent(object sender, EventArgs e);


而且,您会注意到,通过在此签名的前面加上 void,我们被迫在我们自己的处理程序中使用 void 来订阅事件。


这意味着如果你想让你的处理程序运行异步/等待代码,你需要在你的 void 方法中等待......这引入了可怕的异步无效模式,我们被告知要像瘟疫一样避免。


为什么?因为 async void 破坏了异常正常冒泡的能力,并可能因此导致大量头痛。


上一篇通过允许我们在事物的调用方面发挥创造力来解决这个问题,但是......


  • 我们可能需要在我们不控制事件调用的对象上支持这一点(即,您正在连接到您最喜欢的 UI 框架中的按钮点击事件)


  • 有些人认为在该解决方案中使用上下文是一种 hack(我也不反对)。


  • ……具体来说,对于事件处理程序,我们可以使用其他一些更简单的技巧来支持异步事件处理程序!


在我看来,越简单越好……因此,如果您阅读了我之前关于 async void 的文章并且您的目标实际上只是处理 EventHandler,那么这应该有所帮助。

使用 Try/Catch 解决异步事件处理程序

根据前面所述的条件,异常处理会在 async void 的边界上崩溃。如果你有一个异常需要冒泡跨越这个边界,那么你将会玩得很开心。


说到乐趣,我的意思是,如果您喜欢调试为什么某些东西无法正常工作并且您没有明确指出发生了什么问题,那么您真的会玩得很开心。


那么解决这个问题最简单的方法是什么?


让我们首先使用一个我们可以访问的简单工具来防止异常跨越这个边界:try/catch。


 objectThatRaisesEvent.TheEvent += async (s, e) => { // if the try catch surrounds EVERYTHING in the handler, no exception can bubble up try { await SomeTaskYouWantToAwait(); } catch (Exception ex) { // TODO: put your exception handling stuff here } // no exception can escape here if the try/catch surrounds the entire handler body }


如上面的代码所述,如果您在事件处理程序的整个主体周围放置一个 try/catch 块,那么您可以防止任何异常冒泡跨越该异步无效边界。从表面上看,它非常简单,不需要任何花哨的东西即可实现。


优点:

  • 极其简单。没有需要理解的复杂机制。


  • 不需要包裹。


  • 您无需成为引发事件的类的所有者即可使其工作。这意味着这种方法适用于所有现有的事件引发对象,包括 WinForms 和 WPF UI 组件。


缺点:

  • 你需要记住这样做......无处不在。


  • 随着您的代码随着时间的推移而发展,有人可能会不小心在事件处理程序的 try-catch 之外编写可能引发异常的逻辑


话虽如此,这个解决方案确实很简单,但我认为我们可以做得更好一点。

改进异步事件处理程序的(稍微)更高级的方法

我认为我们可以对最初提出的解决方案做出的一项改进是,我们可以更明确一点,我们有一个异步 EventHandler,它应该是安全的,不会冒出异常。


这种方法还将防止代码随时间漂移导致有问题的代码在事件处理程序之外运行。但是,它不会解决您需要记住手动添加的事实!


让我们看看代码:

 static class EventHandlers { public static EventHandler<TArgs> TryAsync<TArgs>( Func<object, TArgs, Task> callback, Action<Exception> errorHandler) where TArgs : EventArgs => TryAsync<TArgs>( callback, ex => { errorHandler.Invoke(ex); return Task.CompletedTask; }); public static EventHandler<TArgs> TryAsync<TArgs>( Func<object, TArgs, Task> callback, Func<Exception, Task> errorHandler) where TArgs : EventArgs { return new EventHandler<TArgs>(async (object s, TArgs e) => { try { await callback.Invoke(s, e); } catch (Exception ex) { await errorHandler.Invoke(ex); } }); } }


上面的代码完全使用了完全相同的方法来防止异常跨越 async void 边界。我们只是尝试捕获事件处理程序的主体,但现在我们已将其捆绑到一个明确的专用方法中以供重用。


以下是它的应用方式:


 someEventRaisingObject.TheEvent += EventHandlers.TryAsync<EventArgs>( async (s, e) => { Console.WriteLine("Starting the event handler..."); await SomeTaskToAwait(); Console.WriteLine("Event handler completed."); }, ex => Console.WriteLine($"[TryAsync Error Callback] Our exception handler caught: {ex}"));


我们可以看到我们现在有一个带有异步任务签名的委托可以使用,我们放心放入其中的任何东西都会在我们之前看到的帮助程序方法中围绕它进行 try/catch。


这是显示正确捕获异常的错误处理程序回调的屏幕截图:


异步事件处理程序示例程序的输出

优点:


  • 还是很简单的。包装函数*稍微*复杂,但仍然非常基本。


  • 不需要包裹。


  • 您无需成为引发事件的类的所有者即可使其工作。这意味着这种方法适用于所有现有的事件引发对象,包括 WinForms 和 WPF UI 组件。


  • 由于将处理程序连接到事件时的语法,使用异步事件处理程序的意图更为明显。


  • 最终抛出更多异常的代码漂移仍将包含在 try/catch 中


缺点:


  • 你还是要记得把这东西挂起来!

结束对异步事件处理程序的思考

虽然最初我着手探索处理 async void 的有趣方法,读者的反馈是有效的,因为示例侧重于异步事件处理程序,并且肯定有更简单的方法。


在本文中,我们探讨了我可能认为是使异步事件处理程序正常运行的最简单方法,而改进的解决方案(在我看来)唯一的缺点是您需要记住使用它。


一位评论者建议可以探索面向方面编程(AoP) 在您的应用程序中注入这种行为,这样您就不需要记住要这样做了。


存在一些编译时 AoP 框架,但我将把它作为读者的练习留给你(因为这也是我要跟进的练习)。