目录
本文将帮助您更好地浏览长时间的C#调试会话,其中功能神秘地停止在应用程序中工作。
作为C#程序员,我们都已经被异步EventHandler烧毁了。如果您还没有,那么希望本文能够让您能够更好地导航长时间的调试会话,其中功能神秘地停止在您的应用程序中工作。虽然有几种不同的解决方案来处理异步事件处理程序,要么避免使用异步void,要么甚至采用异步void,但在本文中,我们将探讨另一个选项,即Task EventHandler。
问题的根源
C#中的普通EventHandler在其签名中具有返回void类型。这不是一个问题,直到我们想要连接一个标记为async的EventHandler,因为我们想await一些在内部调用的Task。
为什么这是一个问题?因为async void会破坏异常正常冒泡的能力,并且当您无法跟踪异常流向何处时,可能会导致调试噩梦。
从根本上说,我们遇到的复杂情况只是因为EventHandler的签名具有返回void类型,这破坏了异常控制:
void TheObject_TheEvent(object sender, EventArgs e);
但是,如果我们能解决这个问题呢?
任务事件处理程序可以工作!
诚然,我们将要深入研究的特定解决方案的用例比我之前提到的其他一些解决方案更有限。但是,我仍然认为,当您可以控制要添加到类中的事件并且我们了解其局限性时,这是一个非常可行的解决方案。也就是说,如果要创建一个类并定义希望调用方能够订阅的自己的事件,则此解决方案可能适合您。之后我们将讨论另一个缺点,这对于理解和了解所有事情很重要,我认为在做出决定之前了解利弊很重要。
根据上一节中所说的,我们可以在这里尝试解决的问题是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
{
// you could in theory have your own event args here
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。我们可以看到下面的结果:
Catch
这个实现有一个很大的问题,不幸的是,对于我的许多用例来说,它使它成为一个交易破坏者。但是,可能会有一些创造性的解决方法,具体取决于您要完成的任务。
Event和EventHandler不像回调那样*完全*操作。我们从它们那里获得的+/-语法允许我们在调用列表中添加和删除处理程序。任务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);
}
};
}
// FIXME: This needs some thought because the
// anonymous method signature we used for try/catch
// support breaks this
remove { _ExplicitAsyncEvent -= value; }
}
在上面的代码中,我们实际上使用了本文中的技巧,将处理程序包装在try/catch中。以try/catch这种方式分层会围绕您打算对异常执行的操作产生其他复杂性,并且解钩事件也会变得更加复杂。
任务事件处理程序...值得吗?
总而言之,在本文中,我们研究了一种可以完全避免使用async void。此解决方案的要点是,您可以控制定义要订阅的事件的类,并且了解多个处理程序质询。如果您尝试为已定义这些事件的类解决async EventHandler的异常处理,则此解决方案将不起作用。为此,您可能需要查看本文或本文。
如果我发现自己想要使用事件而不是回调并且可以保证单个订阅者,这可能是一个可行的选择。但是,鉴于我设计的许多系统可能希望支持N个订阅者,对于我编写的大部分内容来说,Task EventHandler可能并不可行。
本文最初发表于 https://www.devleader.ca/2023/02/18/task-eventhandlers-the-little-secret-you-not-know。
https://www.codeproject.com/Articles/5354964/Task-EventHandlers-The-Little-Secret-You-Didn-t-Kn