当我们讨论异步事件处理程序时,我们很多人首先想到的是它是我们似乎允许的唯一例外
当我之前写过这篇文章时,我很兴奋,因为我正在探索一种解决方案,该解决方案实际上允许 async void 存在(不想把我剩下的头发拉出来)。
对我来说,这更多是关于我们可以用来克服异步事件处理程序的一些巧妙技巧,而不是提供完全避免该问题的解决方案。
话虽如此,这篇文章引起了很多关注,对此我深表感谢,一些人表达了他们宁愿以不同方式解决异步事件处理程序的意见。
我认为这是一个很好的观点,所以我想提出一种替代方法,它不会修复 async void,但它允许你在解决一些挑战的同时完全避免它(看看我在那里做了什么?)使用异步事件处理程序。
在本文中,我将介绍另一种解决方案,您可以在自己的代码中进行尝试。我们将从我的角度阐述关于如何使用它的利弊,以便您可以决定它是否对您的用例有意义。
您还可以在 .NET fiddle 上找到一些可交互的代码
我们面临的异步事件处理程序问题是,我们可以在 C# 中默认订阅的事件签名如下所示:
void TheObject_TheEvent(object sender, EventArgs e);
而且,您会注意到,通过在此签名的前面加上 void,我们被迫在我们自己的处理程序中使用 void 来订阅事件。
这意味着如果你想让你的处理程序运行异步/等待代码,你需要在你的 void 方法中等待......这引入了可怕的异步无效模式,我们被告知要像瘟疫一样避免。
为什么?因为 async void 破坏了异常正常冒泡的能力,并可能因此导致大量头痛。
在我看来,越简单越好……因此,如果您阅读了我之前关于 async void 的文章并且您的目标实际上只是处理 EventHandler,那么这应该有所帮助。
根据前面所述的条件,异常处理会在 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 块,那么您可以防止任何异常冒泡跨越该异步无效边界。从表面上看,它非常简单,不需要任何花哨的东西即可实现。
优点:
缺点:
话虽如此,这个解决方案确实很简单,但我认为我们可以做得更好一点。
我认为我们可以对最初提出的解决方案做出的一项改进是,我们可以更明确一点,我们有一个异步 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。
这是显示正确捕获异常的错误处理程序回调的屏幕截图:
优点:
缺点:
虽然最初我着手探索
在本文中,我们探讨了我可能认为是使异步事件处理程序正常运行的最简单方法,而改进的解决方案(在我看来)唯一的缺点是您需要记住使用它。
一位评论者建议可以探索
存在一些编译时 AoP 框架,但我将把它作为读者的练习留给你(因为这也是我要跟进的练习)。