Quando discutimos EventHandlers assíncronos, a primeira coisa que vem à mente de muitos de nós é que é a única exceção que parece permitir
Quando escrevi sobre isso antes, fiquei empolgado por estar explorando uma solução que envolvia realmente permitir que o vazio assíncrono existisse (sem querer arrancar o resto do meu cabelo).
Para mim, isso foi muito mais sobre alguns truques inteligentes que podemos usar para superar EventHandlers assíncronos do que fornecer soluções para evitar o problema completamente.
Com isso dito, porém, houve muita tração no artigo, pelo qual sou muito grato, e algumas pessoas expressaram opiniões de que preferem resolver EventHandlers assíncronos de uma maneira diferente.
Eu pensei que este era um ótimo ponto, então eu queria criar uma abordagem alternativa que não corrija o vazio assíncrono, mas permite que você o evite (viu o que eu fiz lá?) inteiramente ao resolver alguns dos desafios com EventHandlers assíncronos.
Neste artigo, apresentarei outra solução que você pode experimentar em seu próprio código. Abordaremos os prós e os contras da minha perspectiva com relação a como ele pode ser usado para que você decida se faz sentido para o seu caso de uso.
Você também pode encontrar algum código interativo no violino .NET à direita
O problema que enfrentamos com EventHandlers assíncronos é que a assinatura para eventos que podemos assinar em C# por padrão é algo como isto:
void TheObject_TheEvent(object sender, EventArgs e);
E você notará que, ao colocar void na frente dessa assinatura, somos forçados a usar void em nossos próprios manipuladores para assinar o evento.
Isso significa que, se você quiser que seu manipulador execute o código async/await, você precisará aguardar dentro do seu método void… O que introduz o grande e assustador padrão void assíncrono que nos dizem para evitar como a praga.
E porque? Porque o async void interrompe a capacidade de as exceções surgirem corretamente e pode causar uma tonelada de dores de cabeça como resultado.
Na minha opinião, simples é melhor... então, se você leu meu artigo anterior sobre async void e seu objetivo era apenas lidar com EventHandlers, isso deve ajudar.
Com base nas condições declaradas anteriormente, o tratamento de exceção é interrompido no limite do vazio assíncrono. Se você tiver uma exceção que precisa borbulhar cruzando esse limite, você terá um momento divertido.
E por diversão, quero dizer, se você gosta de depurar por que as coisas não estão funcionando e não tem uma indicação clara do que está acontecendo, então você realmente se divertirá.
Então, qual é a maneira mais fácil de corrigir isso?
Vamos evitar que as exceções ultrapassem esse limite em primeiro lugar, usando uma ferramenta simples à qual temos acesso: 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 }
Conforme observado no código acima, se você colocar um bloco try/catch em TODO o corpo do seu manipulador de eventos, poderá impedir que qualquer exceção borbulhe além desse limite vazio assíncrono. Superficialmente, é bastante simples e não requer nada sofisticado para implementá-lo.
Prós:
Contras:
Com isso dito, esta solução é realmente simples, mas acho que podemos fazer um pouco melhor.
Uma melhoria que acho que podemos fazer sobre a solução proposta inicialmente é que podemos tornar um pouco mais explícito que temos um EventHandler assíncrono que deve estar protegido contra exceções borbulhantes.
Essa abordagem também impedirá que o desvio de código ao longo do tempo cause a execução de códigos problemáticos fora do manipulador de eventos. No entanto, não abordará o fato de que você precisa se lembrar de adicionar isso manualmente!
Vamos verificar o código:
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); } }); } }
O código acima literalmente usa exatamente a mesma abordagem para impedir que exceções cruzem o limite do vazio assíncrono. Simplesmente tentamos capturar o corpo do manipulador de eventos, mas agora o agrupamos em um método explicitamente dedicado para reutilização.
Veja como seria aplicá-lo:
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}"));
Podemos ver que agora temos um delegado com uma assinatura de tarefa assíncrona para trabalhar, e qualquer coisa que colocarmos dentro dela, temos certeza de que terá um try/catch dentro do método auxiliar que vimos anteriormente.
Aqui está uma captura de tela mostrando o retorno de chamada do manipulador de erros capturando corretamente a exceção:
Prós:
Contras:
Enquanto originalmente eu me propus a explorar
Neste artigo, exploramos o que eu poderia argumentar ser a maneira mais simples de fazer com que seus EventHandlers assíncronos se comportem adequadamente, e a solução refinada (na minha opinião) tem apenas a desvantagem de que você precisa se lembrar de usá-la.
Um comentarista sugeriu que alguém poderia explorar
Existem algumas estruturas AoP em tempo de compilação, mas vou deixar isso como um exercício para você como leitor (porque também é um exercício para eu acompanhar).