Por que tentar {…} finalmente {…} é bom; tentar {…} pegar {} ruim?

201

Vi pessoas dizerem que é uma má forma usar catch sem argumentos, especialmente se esse catch não fizer nada:

StreamReader reader=new  StreamReader("myfile.txt");
try
{
  int i = 5 / 0;
}
catch   // No args, so it will catch any exception
{}
reader.Close();

No entanto, isso é considerado uma boa forma:

StreamReader reader=new  StreamReader("myfile.txt");
try
{
  int i = 5 / 0;
}
finally   // Will execute despite any exception
{
  reader.Close();
}

Até onde eu sei, a única diferença entre colocar o código de limpeza em um bloco finalmente e colocar o código de limpeza após os blocos try..catch é se você tiver instruções de retorno no bloco try (nesse caso, o código de limpeza finalmente será executar, mas o código após o try..catch não será).

Caso contrário, o que há de tão especial finalmente?

mbeckish
fonte
7
Antes de tentar pegar um tigre com o qual não possa lidar, você deve documentar seus desejos finais.
O tópico Exceções na documentação pode fornecer algumas boas idéias. Veja também o exemplo do Finalmente bloco .
Athafoud 20/09/16

Respostas:

357

A grande diferença é que isso try...catchirá engolir a exceção, ocultando o fato de que ocorreu um erro. try..finallyexecutará seu código de limpeza e a exceção continuará, sendo manipulada por algo que sabe o que fazer com ele.

Khoth
fonte
11
Qualquer código escrito com o encapsulamento em mente provavelmente só poderá lidar com a exceção no ponto em que é gerado. Simplesmente devolvê-lo à pilha de chamadas na esperança desesperada de que algo mais possa lidar com alguma exceção arbitrária é uma receita para o desastre.
David Arno
3
Na maioria dos casos, é mais evidente o motivo pelo qual uma exceção específica ocorreria no nível do aplicativo (por exemplo, uma determinada configuração) do que no nível do nível da biblioteca.
Mark Cidade
88
David - Prefiro que o programa falhe rapidamente, para que eu fique ciente do problema, em vez de deixar o programa em execução em um estado desconhecido.
Erik Forbes
6
Se o seu programa estiver em um estado desconhecido após uma exceção, você estará fazendo o código errado.
Zan Lynx
41
@ DavidArno, qualquer código escrito com o encapsulamento em mente deve lidar apenas com a exceção dentro de seu escopo. Qualquer outra coisa deve ser repassada para outra pessoa lidar. Se eu tiver um aplicativo que receba um nome de arquivo dos usuários e depois o ler, e meu leitor de arquivos receber uma exceção ao abrir o arquivo, ele deve passá-los (ou consumir a exceção e lançar um novo) para que o aplicativo possa dizer , ei - o arquivo não abriu, vamos solicitar ao usuário um diferente. O leitor de arquivos não deve poder solicitar aos usuários ou executar outras ações em resposta. Seu único objetivo é ler arquivos.
iheanyi
62

"Finalmente" é uma declaração de "Algo que você deve sempre fazer para garantir que o estado do programa esteja correto". Como tal, é sempre bom ter um, se houver possibilidade de que exceções possam prejudicar o estado do programa. O compilador também se esforça muito para garantir que seu código Finalmente seja executado.

"Catch" é uma declaração de "Eu posso me recuperar dessa exceção". Você só deve se recuperar das exceções que realmente pode corrigir - catch sem argumentos diz "Ei, eu posso me recuperar de qualquer coisa!", O que quase sempre é falso.

Se fosse possível recuperar de todas as exceções, seria realmente uma queixa semântica sobre o que você está declarando sua intenção. No entanto, não é, e quase certamente os quadros acima dos seus estarão mais bem equipados para lidar com certas exceções. Como tal, use finalmente, faça com que seu código de limpeza seja executado gratuitamente, mas ainda assim permita que manipuladores mais experientes lidem com o problema.

Adam Wright
fonte
1
Seu sentimento é generalizado, mas infelizmente ignora outro caso importante: invalidar expressamente um objeto cujos invariantes podem não ser mais válidos. Um padrão comum é o código adquirir um bloqueio, fazer algumas alterações em um objeto e liberar o bloqueio. Se ocorrer uma exceção após fazer algumas alterações, mas não todas, o objeto poderá ser deixado em um estado inválido. Mesmo que IMHO melhores alternativas devem existir, eu não conheço nenhuma abordagem melhor do que pegar qualquer exceção que ocorre quando o estado do objeto pode ser inválido, expressamente invalidar o estado, e relançar.
Supercat 20/05
32

Porque quando essa única linha lança uma exceção, você não saberia.

Com o primeiro bloco de código, a exceção será simplesmente absorvida , o programa continuará sendo executado mesmo quando o estado do programa estiver errado.

Com o segundo bloco, a exceção será lançada e borbulha , mas o reader.Close()ainda é garantido para ser executado.

Se uma exceção não for esperada, não coloque um bloco try..catch, pois será difícil depurar mais tarde quando o programa estiver em um estado ruim e você não tiver uma idéia do porquê.

chakrit
fonte
21

Finalmente é executado, não importa o quê. Portanto, se seu bloco try foi bem-sucedido, ele será executado; se seu bloco try falhar, ele executará o bloco catch e, em seguida, o bloco final.

Além disso, é melhor tentar usar a seguinte construção:

using (StreamReader reader=new  StreamReader("myfile.txt"))
{
}

Como a instrução using é automaticamente envolvida em uma tentativa / finalmente e o fluxo será fechado automaticamente. (Você precisará colocar uma tentativa / captura em torno da instrução using se realmente deseja capturar a exceção).

Mark Ingram
fonte
5
Isso não está correto. Usando não quebra o código com try / catch, ele deve dizer try / finally
pr0nin
8

Embora os dois blocos de código a seguir sejam equivalentes, eles não são iguais.

try
{
  int i = 1/0; 
}
catch
{
  reader.Close();
  throw;
}

try
{
  int i = 1/0;
}
finally
{
  reader.Close();
}
  1. 'finalmente' é um código que revela a intenção. Você declara ao compilador e a outros programadores que esse código precisa executar, não importa o quê.
  2. se você tiver vários blocos catch e código de limpeza, precisará finalmente. Sem finalmente, você duplicaria seu código de limpeza em cada bloco de captura. (Princípio DRY)

finalmente os blocos são especiais. O CLR reconhece e trata o código dentro de um bloco final separadamente dos blocos de captura, e o CLR se esforça bastante para garantir que um bloco finalmente seja executado sempre. Não é apenas o açúcar sintático do compilador.

Robert Paulson
fonte
5

Concordo com o que parece ser o consenso aqui - uma 'captura' vazia é ruim porque mascara qualquer exceção que possa ter ocorrido no bloco try.

Além disso, do ponto de vista da legibilidade, quando vejo um bloco 'try', presumo que haverá uma instrução 'catch' correspondente. Se você estiver usando apenas uma 'tentativa' para garantir que os recursos sejam desalocados no bloco 'finalmente', considere a instrução 'using' :

using (StreamReader reader = new StreamReader('myfile.txt'))
{
    // do stuff here
} // reader.dispose() is called automatically

Você pode usar a instrução 'using' com qualquer objeto que implemente IDisposable. O método dispose () do objeto é chamado automaticamente no final do bloco.

Chris Lawlor
fonte
4

Use Try..Catch..Finally, se o seu método souber como lidar com a exceção localmente. A exceção ocorre em Try, Handled in Catch e depois que a limpeza é feita em Finalmente.

Caso seu método não saiba como lidar com a exceção, mas precise de uma limpeza após a ocorrência, use Try..Finally

Por isso, a exceção é propagada para os métodos de chamada e manipulada se houver alguma instrução Catch adequada nos métodos de chamada. Se não houver manipuladores de exceção no método atual ou em qualquer um dos métodos de chamada, o aplicativo trava.

Por Try..Finallyisso, é garantido que a limpeza local seja feita antes de propagar a exceção para os métodos de chamada.

manjuv
fonte
1
Por mais básica que seja essa resposta, é absolutamente a melhor. É bom ter o hábito de tentar / pegar / finalmente, mesmo que um dos dois últimos seja deixado vazio. Existem circunstâncias MUITO RARAS em que um bloco catch pode existir e estar vazio, mas pelo menos se você sempre escrever try / catch / finalmente, verá o bloco vazio enquanto lê o código. Ter um bloco final vazio é útil da mesma maneira. Se você precisar de limpeza mais tarde ou precisar depurar um estado no momento da exceção, é incrivelmente útil.
Jesse Williams
3

O bloco try..finally ainda lançará quaisquer exceções levantadas. Tudo o finallyque é necessário é garantir que o código de limpeza seja executado antes que a exceção seja lançada.

O try..catch com uma captura vazia consumirá completamente qualquer exceção e oculta o fato de que aconteceu. O leitor estará fechado, mas não há como saber se a coisa certa aconteceu. E se sua intenção fosse gravar i no arquivo? Nesse caso, você não chegará a essa parte do código e o myfile.txt estará vazio. Todos os métodos a jusante lidam com isso corretamente? Quando vir o arquivo vazio, você poderá adivinhar corretamente que ele está vazio porque uma exceção foi lançada? É melhor lançar a exceção e informar que você está fazendo algo errado.

Outro motivo é o try..catch feito dessa maneira está completamente incorreto. O que você está dizendo ao fazer isso é: "Não importa o que aconteça, eu posso lidar com isso". Que tal StackOverflowException, você pode limpar depois disso? Que tal OutOfMemoryException? Em geral, você deve lidar apenas com as exceções que espera e sabe como lidar.

OwenP
fonte
2

Se você não souber que tipo de exceção capturar ou o que fazer com ele, não há sentido em ter uma instrução catch. Você deve deixar para um chamador superior que possa ter mais informações sobre a situação para saber o que fazer.

Você ainda deve ter uma declaração finalmente, caso haja uma exceção, para poder limpar os recursos antes que essa exceção seja lançada para o chamador.

Mark Cidade
fonte
2

Do ponto de vista da legibilidade, é mais explícito dizer aos futuros leitores de código "que esse material aqui é importante, precisa ser feito, aconteça o que acontecer". Isso é bom.

Além disso, as declarações de captura vazias tendem a ter um certo "cheiro" para elas. Eles podem ser um sinal de que os desenvolvedores não estão pensando nas várias exceções que podem ocorrer e em como lidar com elas.

Ryan
fonte
2

Finalmente, é opcional - não há razão para ter um bloco "Finalmente" se não houver recursos para limpar.

Guy Starbuck
fonte
2

Retirado de: aqui

Gerar e capturar exceções não deve ocorrer rotineiramente como parte da execução bem-sucedida de um método. Ao desenvolver bibliotecas de classes, o código do cliente deve ter a oportunidade de testar uma condição de erro antes de executar uma operação que possa resultar em uma exceção. Por exemplo, System.IO.FileStream fornece uma propriedade CanRead que pode ser verificada antes de chamar o método Read, evitando que uma possível exceção seja gerada, conforme ilustrado no seguinte trecho de código:

Dim str As Stream = GetStream () If (str.CanRead) Then 'código para ler o fluxo End If

A decisão de verificar o estado de um objeto antes de chamar um método específico que pode gerar uma exceção depende do estado esperado do objeto. Se um objeto FileStream for criado usando um caminho de arquivo que deveria existir e um construtor que retornasse um arquivo no modo de leitura, a verificação da propriedade CanRead não é necessária; a incapacidade de ler o FileStream seria uma violação do comportamento esperado das chamadas de método feitas e uma exceção deveria ser gerada. Por outro lado, se um método estiver documentado como retornando uma referência FileStream que pode ou não ser legível, é recomendável verificar a propriedade CanRead antes de tentar ler os dados.

Para ilustrar o impacto no desempenho que o uso de uma técnica de codificação "executar até exceção" pode causar, o desempenho de uma conversão, que lança uma InvalidCastException se a conversão falhar, é comparado ao C # como operador, que retorna nulos se uma conversão falhar. O desempenho das duas técnicas é idêntico para o caso em que a conversão é válida (consulte o Teste 8.05), mas para o caso em que a conversão é inválida e o uso de uma conversão causa uma exceção, o uso da conversão é 600 vezes mais lento que o uso do método como operador (consulte Teste 8.06). O impacto de alto desempenho da técnica de lançamento de exceção inclui o custo de alocar, lançar e capturar a exceção e o custo da coleta de lixo subsequente do objeto de exceção, o que significa que o impacto instantâneo de lançar uma exceção não é tão alto. Quanto mais exceções forem lançadas,

SpoiledTechie.com
fonte
2
Scott - se o texto que você citou acima estiver atrás do paywall de expertsexchange.com, você provavelmente não deve publicá-lo aqui. Posso estar errado nisso, mas aposto que não é uma boa ideia.
Onorio Catenacci
2

É uma prática ruim adicionar uma cláusula catch apenas para repetir a exceção.

Bastien Vandamme
fonte
2

Se você ler C # para programadores , entenderá que o bloco final foi projetado para otimizar um aplicativo e impedir o vazamento de memória.

O CLR não elimina completamente os vazamentos ... podem ocorrer vazamentos de memória se o programa inadvertidamente manter referências a objetos indesejados

Por exemplo, quando você abre uma conexão de arquivo ou banco de dados, sua máquina alocará memória para atender a essa transação, e essa memória será mantida não, a menos que o comando descartado ou próximo tenha sido executado. mas se durante a transação ocorreu um erro, o comando de continuação será encerrado, a menos que esteja dentro do try.. finally..bloco.

catchera diferente finallyno sentido de que catch foi projetado para permitir que você lida com / gerencie ou interprete o erro. Pense nisso como uma pessoa que diz "ei, peguei alguns bandidos, o que você quer que eu faça com eles?" while finallyfoi projetado para garantir que seus recursos foram colocados corretamente. Pense em alguém que, havendo ou não alguns bandidos, ele garantirá que sua propriedade ainda esteja segura.

E você deve permitir que esses dois trabalhem juntos para sempre.

por exemplo:

try
{
  StreamReader reader=new  StreamReader("myfile.txt");
  //do other stuff
}
catch(Exception ex){
 // Create log, or show notification
 generic.Createlog("Error", ex.message);
}
finally   // Will execute despite any exception
{
  reader.Close();
}
dr.Crow
fonte
1

Com finalmente, você pode limpar os recursos, mesmo que sua declaração de captura gere a exceção até o programa de chamada. Com o seu exemplo contendo a instrução catch vazia, há pouca diferença. No entanto, se na sua captura, você faz algum processamento e lança o erro, ou mesmo nem sequer tem uma captura, o finalmente ainda será executado.

Kibbee
fonte
1

Bem, por um lado, é uma prática ruim capturar exceções que você não se importa em lidar. Confira o Capítulo 5 sobre o desempenho do .Net em Melhorando o desempenho e a escalabilidade do aplicativo .NET . Nota: você provavelmente deveria estar carregando o fluxo dentro do bloco try, dessa forma, poderá capturar a exceção pertinente se falhar. Criar o fluxo fora do bloco try anula seu objetivo.

Fator Místico
fonte
0

Entre provavelmente muitos motivos, as exceções são muito lentas para executar. Você pode facilmente prejudicar seus tempos de execução, se isso acontecer muito.

lotes fora do tempo
fonte
0

O problema com os blocos try / catch que capturam todas as exceções é que seu programa está agora em um estado indeterminado se ocorrer uma exceção desconhecida. Isso é totalmente contrário à regra de falha rápida - você não deseja que seu programa continue se ocorrer uma exceção. A tentativa / captura acima até capturaria OutOfMemoryExceptions, mas esse é definitivamente um estado em que seu programa não será executado.

Os blocos try / finalmente permitem executar o código de limpeza enquanto ainda falham rapidamente. Na maioria das circunstâncias, você deseja capturar todas as exceções no nível global, para que você possa registrá-las e sair.

David Mohundro
fonte
0

A diferença efetiva entre seus exemplos é insignificante, desde que nenhuma exceção seja lançada.

Se, no entanto, uma exceção for lançada enquanto estiver na cláusula 'try', o primeiro exemplo a engolirá completamente. O segundo exemplo aumentará a exceção para a próxima etapa da pilha de chamadas, portanto a diferença nos exemplos declarados é que um oculta completamente todas as exceções (primeiro exemplo) e o outro (segundo exemplo) retém as informações de exceção para possível tratamento posterior enquanto ainda executando o conteúdo na cláusula 'finally'.

Se, por exemplo, você colocasse o código na cláusula 'catch' do primeiro exemplo que gerou uma exceção (aquela que foi inicialmente levantada ou uma nova), o código de limpeza do leitor nunca seria executado. Finalmente, executa independentemente do que acontece na cláusula 'catch'.

Portanto, a principal diferença entre 'catch' e 'finalmente' é que o conteúdo do bloco 'finally' (com algumas raras exceções) pode ser considerado com garantia de execução, mesmo diante de uma exceção inesperada, enquanto qualquer código a seguir uma cláusula de 'captura' (mas fora de uma cláusula 'finalmente') não teria essa garantia.

Aliás, Stream e StreamReader implementam IDisposable e podem ser agrupados em um bloco 'using'. Blocos 'Using' são o equivalente semântico de try / finalmente (sem 'catch'); portanto, seu exemplo pode ser expresso de forma mais concisa como:

using (StreamReader reader = new  StreamReader("myfile.txt"))
{
  int i = 5 / 0;
}

... que fechará e descartará a instância StreamReader quando ela estiver fora do escopo. Espero que isto ajude.

Jared
fonte
0

tentar {…} pegar {} nem sempre é ruim. Não é um padrão comum, mas costumo usá-lo quando preciso encerrar recursos, não importa como, como fechar um (possivelmente) soquete aberto no final de um encadeamento.

Martin Liesén
fonte