Durante a mudança para o novo .NET Core 3 IAsynsDisposable
, deparei-me com o seguinte problema.
O núcleo do problema: se DisposeAsync
lança uma exceção, essa exceção oculta todas as exceções lançadas dentro de await using
-block.
class Program
{
static async Task Main()
{
try
{
await using (var d = new D())
{
throw new ArgumentException("I'm inside using");
}
}
catch (Exception e)
{
Console.WriteLine(e.Message); // prints I'm inside dispose
}
}
}
class D : IAsyncDisposable
{
public async ValueTask DisposeAsync()
{
await Task.Delay(1);
throw new Exception("I'm inside dispose");
}
}
O que está sendo pego é a AsyncDispose
exceção-se lançada, e a exceção de dentro await using
somente se AsyncDispose
não for lançada .
No entanto, eu preferiria o contrário: obter a exceção do await using
bloco, se possível, e DisposeAsync
-exception somente se o await using
bloco for concluído com êxito.
Justificativa: imagine que minha classe D
funcione com alguns recursos de rede e assine algumas notificações remotas. O código interno await using
pode fazer algo errado e falhar no canal de comunicação; depois disso, o código em Dispose, que tenta fechar normalmente a comunicação (por exemplo, cancelar a assinatura das notificações) também falharia. Mas a primeira exceção fornece informações reais sobre o problema, e a segunda é apenas um problema secundário.
No outro caso, quando a parte principal foi executada e a eliminação falhou, o problema real está dentro DisposeAsync
, portanto a exceção DisposeAsync
é a relevante. Isso significa que apenas suprimir todas as exceções internas DisposeAsync
não deve ser uma boa ideia.
Eu sei que existe o mesmo problema com o caso não assíncrono: a exceção finally
substitui a exceção try
, é por isso que não é recomendável lançar Dispose()
. Mas, com as classes de acesso à rede, suprimir exceções nos métodos de fechamento não parece bom.
É possível solucionar o problema com o seguinte auxiliar:
static class AsyncTools
{
public static async Task UsingAsync<T>(this T disposable, Func<T, Task> task)
where T : IAsyncDisposable
{
bool trySucceeded = false;
try
{
await task(disposable);
trySucceeded = true;
}
finally
{
if (trySucceeded)
await disposable.DisposeAsync();
else // must suppress exceptions
try { await disposable.DisposeAsync(); } catch { }
}
}
}
e use-o como
await new D().UsingAsync(d =>
{
throw new ArgumentException("I'm inside using");
});
que é meio feio (e não permite coisas como retornos antecipados dentro do bloco using).
Existe uma solução boa e canônica, await using
se possível? Minha pesquisa na internet não encontrou nem discutir esse problema.
Close
método separado por esse motivo. Provavelmente, é aconselhável fazer o mesmo:CloseAsync
tentativas de fechar bem as coisas e fracassar.DisposeAsync
apenas faz o seu melhor e falha silenciosamente.CloseAsync
meio separado significa que preciso tomar precauções extras para fazê-lo funcionar. Se eu apenas colocá-lo no final dousing
bloco, ele será ignorado nos retornos antecipados etc. (é isso que gostaríamos que acontecesse) e exceções (é isso que gostaríamos que acontecesse). Mas a ideia parece promissora.Dispose
sempre foi "As coisas podem ter dado errado: basta fazer o seu melhor para melhorar a situação, mas não piorar", e não vejo por queAsyncDispose
deveria ser diferente.DisposeAsync
fazer o melhor para arrumar, mas não jogar é a coisa certa a fazer. Você estava falando sobre retornos antecipados intencionais , nos quais um retorno antecipado intencional pode ignorar erroneamente uma chamada paraCloseAsync
: esses são os proibidos por muitos padrões de codificação.Respostas:
Há exceções que você deseja exibir (interrompa a solicitação atual ou interrompa o processo) e há exceções que seu projeto espera que ocorram às vezes e você possa lidar com elas (por exemplo, tente novamente e continue).
Mas a distinção entre esses dois tipos depende do chamador final do código - este é o ponto inteiro das exceções, para deixar a decisão por conta do chamador.
Às vezes, o chamador dará prioridade maior à superfície da exceção do bloco de código original e, às vezes, à exceção do
Dispose
. Não há regra geral para decidir qual deve ter prioridade. O CLR é pelo menos consistente (como você observou) entre o comportamento de sincronização e não-assíncrono.Talvez seja lamentável que agora tenhamos que
AggregateException
representar várias exceções, não possa ser adaptado para resolver isso. ou seja, se uma exceção já estiver em andamento e outra for lançada, elas serão combinadas em umaAggregateException
. Ocatch
mecanismo pode ser modificado para que, se você escrevercatch (MyException)
, ele capture qualquer umAggregateException
que inclua uma exceção do tipoMyException
. Porém, existem várias outras complicações decorrentes dessa idéia e provavelmente é muito arriscado modificar algo tão fundamental agora.Você pode melhorar seu
UsingAsync
suporte ao retorno antecipado de um valor:fonte
await using
pode ser usado (é aqui que o DisposeAsync não será lançado em casos não fatais) e um auxiliar comoUsingAsync
é mais apropriado (se é provável que DisposeAsync seja lançado) ? (Claro, eu preciso modificarUsingAsync
de modo que ele não cegamente pegar tudo, mas só não fatal (e não-boneheaded no uso de Eric Lippert ).)Talvez você já entenda por que isso acontece, mas vale a pena explicar. Esse comportamento não é específico para
await using
. Isso aconteceria com umusing
bloco simples também. Então, enquanto eu digoDispose()
aqui, tudo se aplicaDisposeAsync()
também.Um
using
bloco é apenas um açúcar sintático para umtry
/finally
bloco, como diz a seção de comentários da documentação . O que você vê acontece porque ofinally
bloco sempre é executado, mesmo após uma exceção. Portanto, se uma exceção ocorrer e não houvercatch
bloco, a exceção será colocada em espera até que ofinally
bloco seja executado e a exceção será lançada. Mas se ocorrer uma exceçãofinally
, você nunca verá a exceção antiga.Você pode ver isso com este exemplo:
Não importa se é chamado
Dispose()
ou nãoDisposeAsync()
dentro dofinally
. O comportamento é o mesmo.Meu primeiro pensamento é: não jogue
Dispose()
. Mas depois de revisar parte do código da Microsoft, acho que depende.Veja a implementação deles
FileStream
, por exemplo. Tanto oDispose()
método síncrono , comoDisposeAsync()
pode realmente lançar exceções. O síncronoDispose()
faz ignorar algumas exceções intencionalmente, mas não todos.Mas acho importante levar em conta a natureza da sua turma. Em um
FileStream
, por exemplo,Dispose()
liberará o buffer no sistema de arquivos. Essa é uma tarefa muito importante e você precisa saber se isso falhou . Você não pode simplesmente ignorar isso.No entanto, em outros tipos de objetos, quando você chama
Dispose()
, você realmente não tem mais uso para o objeto. LigarDispose()
realmente significa "esse objeto está morto para mim". Talvez ele limpe alguma memória alocada, mas falhar não afeta a operação do seu aplicativo de forma alguma. Nesse caso, você pode decidir ignorar a exceção dentro do seuDispose()
.Mas, em qualquer caso, se você quiser distinguir entre uma exceção dentro
using
ou uma exceção que veioDispose()
, precisará de um blocotry
/catch
dentro e fora do seuusing
bloco:Ou você simplesmente não pode usar
using
. Escreva um blocotry
/catch
/finally
você mesmo, onde você encontra alguma exceção emfinally
:fonte
catch
interna dousing
bloco não ajudaria, porque geralmente o tratamento de exceções é feito em algum lugar longe dousing
próprio bloco, portanto, o manuseio dentro do blocousing
geralmente não é muito viável. Sobre o uso de nãousing
- é realmente melhor que a solução proposta?try
/catch
/finally
bloquear, pois ficaria imediatamente claro o que está fazendo sem precisar ler o queAsyncUsing
está fazendo. Você também tem a opção de fazer um retorno antecipado. Também haverá um custo adicional de CPU para o seuAwaitUsing
. Seria pequeno, mas está lá.Dispose()
não deve ser lançado porque é chamado mais de uma vez. As implementações da própria Microsoft podem gerar exceções, e por boas razões, como mostrei nesta resposta. No entanto, concordo que você deve evitá-lo se possível, pois normalmente ninguém esperaria que ele fosse lançado.O uso é efetivamente o Código de Tratamento de Exceções (sintaxe sugar para try ... finalmente ... Dispose ()).
Se o seu código de manipulação de exceções estiver lançando Exceções, algo será eliminado.
O que quer que tenha acontecido para levá-lo até lá, não importa mais. O código de manipulação de exceção com defeito oculta todas as exceções possíveis, de uma maneira ou de outra. O código de manipulação de exceção deve ser corrigido, com prioridade absoluta. Sem isso, você nunca obtém dados de depuração suficientes para o problema real. Vejo que isso é feito com extrema frequência, muitas vezes. É tão fácil errar quanto lidar com ponteiros nus. Muitas vezes, existem dois artigos sobre a temática que eu vinculo, que podem ajudá-lo com quaisquer conceitos errôneos de design subjacentes:
Dependendo da classificação de exceção, é isso que você precisa fazer se seu código de manipulação / difusão de exceções gerar uma exceção:
Para Fatal, Boneheaded e Vexing, a solução é a mesma.
Exceções exógenas, devem ser evitadas mesmo a um custo sério. Há uma razão pela qual ainda usamos arquivos de log em vez de registrar bancos de dados para registrar exceções - as operações do banco de dados são uma maneira de propender a problemas exógenos. Os arquivos de log são o único caso, onde eu nem me importo se você mantiver o identificador de arquivo aberto durante todo o tempo de execução.
Se você tiver que fechar uma conexão, não se preocupe muito com a outra extremidade. Manuseie-o como o UDP: "Vou enviar as informações, mas não me importo se o outro lado conseguir." A eliminação é sobre a limpeza de recursos no lado / lado do cliente em que você está trabalhando.
Eu posso tentar notificá-los. Mas limpar as coisas no lado do servidor / FS? É por isso que o tempo limite e o tratamento de exceções são responsáveis.
fonte
NativeMethods.UnmapViewOfFile();
eNativeMethods.CloseHandle()
. Mas esses são importados de fora. Não há verificação de qualquer valor de retorno ou qualquer outra coisa que possa ser usada para obter uma exceção do .NET adequada em torno do que esses dois possam encontrar. Então, eu estou fortemente convencido, SqlConnection.Dispose (bool) simplesmente não se importa. | Fechar é muito melhor, na verdade informando o servidor. Antes de ligar, descarte.Você pode tentar usar AggregateException e modificar seu código da seguinte maneira:
https://docs.microsoft.com/ru-ru/dotnet/api/system.aggregateexception?view=netframework-4.8
https://docs.microsoft.com/ru-ru/dotnet/standard/parallel-programming/exception-handling-task-parallel-library
fonte