Por que usar try ... finalmente sem uma cláusula catch?

79

A maneira clássica de programar é com try ... catch. Quando é apropriado usar trysem catch?

No Python, o seguinte parece legal e pode fazer sentido:

try:
  #do work
finally:
  #do something unconditional

No entanto, o código não fez catchnada. Da mesma forma, pode-se pensar em Java que seria o seguinte:

try {
    //for example try to get a database connection
}
finally {
  //closeConnection(connection)
}

Parece bom e, de repente, não preciso me preocupar com tipos de exceção etc. Se essa é uma boa prática, quando é uma boa prática? Como alternativa, quais são as razões pelas quais isso não é uma boa prática ou não é legal? (Eu não compilei a fonte. Estou perguntando, pois pode ser um erro de sintaxe para Java. Verifiquei se o Python certamente compila.)

Um problema relacionado com o qual me deparei é o seguinte: continuo escrevendo a função / método, no final do qual deve retornar algo. No entanto, pode estar em um local que não deve ser alcançado e deve ser um ponto de retorno. Portanto, mesmo que eu lide com as exceções acima, ainda estou retornando NULLou uma string vazia em algum momento do código que não deve ser alcançado, geralmente o final do método / função. Eu sempre consegui reestruturar o código para que ele não precise return NULL, pois isso parece absolutamente menos do que uma boa prática.

Niklas Rosencrantz
fonte
4
Em Java, por que não colocar a declaração de retorno no final do bloco try?
Kevin cline
2
Estou triste que try..finally e try..catch usem a palavra-chave try, além de ambas começarem com try, são duas construções totalmente diferentes.
Pieter B
3
try/catchnão é "a maneira clássica de programar". É a maneira clássica de programar em C ++ , porque o C ++ não possui uma construção try / finalmente adequada, o que significa que você precisa implementar mudanças de estado reversíveis garantidas usando hacks feios que envolvem RAII. Porém, linguagens OO decentes não têm esse problema, porque fornecem try / finalmente. É usado para uma finalidade muito diferente da tentativa / captura.
precisa saber é o seguinte
3
Eu vejo muito isso com recursos de conexão externa. Você deseja a exceção, mas precisa ter certeza de que não deixa uma conexão aberta, etc.
Rig
4
@MasonWheeler "Feios hacks" , por favor, explique o que é ruim em ter um objeto manipular sua própria limpeza?
precisa saber é o seguinte

Respostas:

146

Depende se você pode lidar com as exceções que podem ser levantadas neste momento ou não.

Se você pode lidar com as exceções localmente, deve fazê-lo e é melhor lidar com o erro o mais próximo possível de onde é gerado.

Se você não puder manipulá-los localmente, basta ter um try / finallybloco perfeitamente razoável - supondo que exista algum código que você precise executar, independentemente de o método ter êxito ou não. Por exemplo (a partir do comentário de Neil ), abrir um fluxo e depois passar esse fluxo para um método interno a ser carregado é um excelente exemplo de quando você precisaria try { } finally { }, usando a cláusula finally para garantir que o fluxo seja fechado independentemente do sucesso ou falha na leitura.

No entanto, você ainda precisará de um manipulador de exceção em algum lugar do seu código - a menos que queira que seu aplicativo trave completamente, é claro. Depende da arquitetura do seu aplicativo exatamente onde está o manipulador.

ChrisF
fonte
8
"e é melhor lidar com o erro o mais próximo possível de onde é gerado." eh, depende. Se você pode se recuperar e ainda assim completar a missão (por assim dizer), sim, é claro. Se você não puder, é melhor deixar a exceção ir até o topo, onde (provavelmente) a intervenção do usuário será necessária para lidar com o que aconteceu.
Será
13
@ will - é por isso que usei a frase "quanto possível".
ChrisF
8
Boa resposta, mas eu acrescentaria um exemplo: abrir um fluxo e passar esse fluxo para um método interno a ser carregado é um excelente exemplo de quando você precisaria try { } finally { }, aproveitando a cláusula finalmente para garantir que o fluxo seja finalmente fechado, independentemente do sucesso / falha.
Neil
porque às vezes todo o caminho em cima é o mais próximo que se pode fazer
Newtopian
"apenas tentar / finalmente bloquear é perfeitamente razoável" estava procurando exatamente essa resposta. Obrigado @ChrisF
neal aise
36

O finallybloco é usado para o código que deve sempre ser executado, independentemente de uma condição de erro (exceção) ocorrer ou não.

O código no finallybloco é executado após a conclusão do trybloco e, se uma exceção detectada ocorrer, após a conclusão do catchbloco correspondente . É sempre executado, mesmo se uma exceção não capturada ocorreu no bloco tryou catch.

O finallybloco é normalmente usado para fechar arquivos, conexões de rede etc. que foram abertos no trybloco. O motivo é que a conexão de arquivo ou rede deve ser fechada, se a operação usando esse arquivo ou conexão de rede foi bem-sucedida ou se falhou.

Deve-se tomar cuidado no finallybloco para garantir que ele próprio não gere uma exceção. Por exemplo, verifique duplamente todas as variáveis null, etc.

yfeldblum
fonte
8
+1: é idiomático para "deve ser limpo". A maioria dos usos de try-finallypode ser substituída por uma withdeclaração.
S.Lott
12
Vários idiomas têm aprimoramentos específicos do idioma extremamente úteis para a try/finallyconstrução. C # using, Python with, etc.
yfeldblum
5
@yfeldblum - existe uma diferença sutil entre usinge try-finally, como o Disposemétodo não será chamado pelo usingbloco se ocorrer uma exceção no IDisposableconstrutor do objeto. try-finallypermite executar código mesmo que o construtor do objeto gere uma exceção.
15118 Scott Whitlock
5
@ScottWhitlock: Isso é uma coisa boa ? O que você está tentando fazer, chame um método em um objeto não construído? Isso é um bilhão de tipos ruins.
23412 DeadMG
1
@DeadMG: Veja também stackoverflow.com/q/14830534/18192
Brian
17

Um exemplo em que try ... finalmente sem uma cláusula catch é apropriado (e mais idiomático ) em Java é o uso do Lock no pacote de bloqueios de utilitários simultâneos.

  • Veja como isso é explicado e justificado na documentação da API (a fonte em negrito entre aspas é minha):

    ... A ausência de bloqueio estruturado em bloco remove a liberação automática de bloqueios que ocorrem com métodos e instruções sincronizados. Na maioria dos casos, o seguinte idioma deve ser usado :

     Lock l = ...;
     l.lock();
     try {
         // access the resource protected by this lock
     } finally {
         l.unlock();
     }
    

    Quando o bloqueio e o desbloqueio ocorrem em escopos diferentes, deve-se tomar cuidado para garantir que todo o código executado enquanto o bloqueio é mantido seja protegido por try-finalmente ou try-catch para garantir que o bloqueio seja liberado quando necessário .

mosquito
fonte
Posso colocar l.lock()dentro de tentar? try{ l.lock(); }finally{l.unlock();}
RMachnik
1
tecnicamente, você pode. Eu não coloquei lá porque semanticamente, faz menos sentido. tryneste trecho é destinado a envolver o acesso a recursos, por poluindo-o com algo relacionado a isso
mosquito
4
Você pode, mas se l.lock()falhar, o finallybloco ainda será executado se l.lock()estiver dentro do trybloco. Se você fizer como o gnat sugere, o finallybloco só será executado quando sabemos que o bloqueio foi adquirido.
Wtrmute 4/03/16
12

Em um nível básico catche finallyresolva dois problemas relacionados, mas diferentes:

  • catch é usado para lidar com um problema relatado pelo código que você chamou
  • finally é usado para limpar dados / recursos que o código atual criou / modificou, não importa se ocorreu ou não um problema

Portanto, ambos estão relacionados de alguma forma a problemas (exceções), mas isso é tudo o que eles têm em comum.

Uma diferença importante é que o finallybloco deve estar no mesmo método em que os recursos foram criados (para evitar vazamentos de recursos) e não pode ser colocado em um nível diferente na pilha de chamadas.

O catchporém é uma questão diferente: o lugar correto para isso depende de onde você pode realmente lidar com a exceção. Não adianta capturar uma exceção em um lugar onde você não pode fazer nada a respeito; portanto, às vezes é melhor simplesmente deixá-la cair.

Joachim Sauer
fonte
2
Nitpick: "... o bloco final deve estar no mesmo método em que os recursos foram criados ..." . Certamente, é uma boa idéia fazê-lo dessa maneira, porque é mais fácil ver que não há vazamento de recursos. No entanto, não é uma pré-condição necessária; ou seja, você não precisa fazer dessa maneira. Você pode liberar o recurso na finallydeclaração try (estática ou dinamicamente) anexa ... e ainda ser 100% à prova de vazamentos.
Stephen C
6

@yfeldblum tem a resposta correta: tente-finalmente sem uma instrução catch geralmente deve ser substituído por uma construção de linguagem apropriada.

Em C ++, ele está usando RAII e construtores / destruidores; em Python é uma withafirmação; e em C #, é uma usingdeclaração.

Elas são quase sempre mais elegantes porque o código de inicialização e finalização está em um local (o objeto abstraído) e não em dois locais.

Neil G
fonte
2
E em Java é ARM bloqueia
MarkJ 15/02/2012
2
Suponha que você tenha um objeto mal projetado (por exemplo, um que não implemente adequadamente IDisposable em C #) que nem sempre é uma opção viável.
mootinator 15/02/2012
1
@mootinator: você não pode herdar do objeto mal projetado e corrigi-lo?
21412 Neil G
2
Ou encapsulamento? Bah. Quero dizer sim, claro.
mootinator
4

Em muitos idiomas, uma finallydeclaração também é executada após a declaração de retorno. Isso significa que você pode fazer algo como:

try {
  // Do processing
  return result;
} finally {
  // Release resources
}

O que libera os recursos, independentemente de como o método foi finalizado com uma exceção ou uma declaração de retorno regular.

Se isso é bom ou ruim, isso está em debate, mas try {} finally {}nem sempre se limita ao tratamento de exceções.

Kamiel Wanrooij
fonte
0

Eu poderia invocar a ira dos Pythonistas (não sei, porque não uso muito o Python) ou programadores de outras linguagens com esta resposta, mas, na minha opinião, a maioria das funções não deveria ter um catchbloco, idealmente falando. Para mostrar o porquê, deixe-me contrastar isso com a propagação manual do código de erro do tipo que eu tive que fazer ao trabalhar com o Turbo C no final dos anos 80 e início dos anos 90.

Então, digamos que temos uma função para carregar uma imagem ou algo parecido em resposta a um usuário selecionar um arquivo de imagem para carregar, e isso está escrito em C e assembly:

insira a descrição da imagem aqui

Omiti algumas funções de baixo nível, mas podemos ver que identifiquei diferentes categorias de funções, codificadas por cores, com base nas responsabilidades que eles têm com relação ao tratamento de erros.

Ponto de falha e recuperação

Agora, nunca foi difícil escrever as categorias de funções que chamo de "ponto possível de falhas" (aquelas que throw, por exemplo) e as funções "recuperação e relatório de erros" (aquelas que catch, por exemplo).

Essas funções eram sempre trivial para escrever corretamente antes de manipulação de exceção estava disponível desde uma função que pode ser executado em uma falha externa, como deixar de alocar a memória, só pode retornar um NULLou 0ou -1ou definir um código de erro global ou algo nesse sentido. E a recuperação / relatório de erros sempre foi fácil, uma vez que você percorreu a pilha de chamadas até um ponto em que fazia sentido recuperar e relatar falhas, basta pegar o código de erro e / ou a mensagem e relatá-lo ao usuário. E, naturalmente, uma função na folha desta hierarquia que nunca pode falhar, não importa como seja alterada no futuro ( Convert Pixel) é simplesmente simples de escrever corretamente (pelo menos no que diz respeito ao tratamento de erros).

Propagação de erros

No entanto, as funções tediosas propensas ao erro humano eram os propagadores de erros , aqueles que não corriam diretamente para a falha, mas denominavam funções que poderiam falhar em algum lugar mais profundo da hierarquia. Nesse ponto, Allocate Scanlinetalvez seja necessário lidar com uma falha malloce retornar um erro para Convert Scanlines, em seguida, Convert Scanlinesteria que verificar esse erro e passá-lo para Decompress Image, then Decompress Image->Parse Imagee and Parse Image->Load Image, e Load Imagepara o comando do usuário no qual o erro foi finalmente relatado .

É aqui que muitos humanos cometem erros, já que é preciso apenas um propagador de erro para não verificar e transmitir o erro para que toda a hierarquia de funções seja derrubada quando se trata de lidar adequadamente com o erro.

Além disso, se códigos de erro são retornados por funções, perdemos praticamente a capacidade de, digamos, 90% de nossa base de código, retornar valores de interesse pelo sucesso, pois muitas funções precisariam reservar seu valor de retorno para retornar um código de erro em falha .

Reduzindo o erro humano: códigos de erro globais

Então, como podemos reduzir a possibilidade de erro humano? Aqui eu posso até invocar a ira de alguns programadores em C, mas uma melhoria imediata na minha opinião é usar códigos de erro globais , como o OpenGL com glGetError. Isso pelo menos libera as funções para retornar valores significativos de interesse pelo sucesso. Existem maneiras de tornar esse thread seguro e eficiente, onde o código de erro está localizado em um thread.

Também existem alguns casos em que uma função pode ter um erro, mas é relativamente inofensivo continuar um pouco mais antes de retornar prematuramente, como resultado da descoberta de um erro anterior. Isso permite que isso aconteça sem ter que verificar se há erros em 90% das chamadas de função feitas em todas as funções, para que ainda possa permitir o tratamento adequado de erros sem ser tão meticuloso.

Reduzindo o erro humano: tratamento de exceções

No entanto, a solução acima ainda exige muitas funções para lidar com o aspecto do fluxo de controle da propagação manual de erros, mesmo que isso possa ter reduzido o número de linhas do if error happened, return errortipo de código manual . Ele não o eliminaria completamente, pois muitas vezes ainda haveria pelo menos um local verificando um erro e retornando para quase todas as funções de propagação de erro. Então é aí que o tratamento de exceções entra em cena para salvar o dia (mais ou menos).

Mas o valor do tratamento de exceções aqui é liberar a necessidade de lidar com o aspecto do fluxo de controle da propagação manual de erros. Isso significa que seu valor está associado à capacidade de evitar a necessidade de escrever uma carga de catchblocos em toda a sua base de código. No diagrama acima, o único local que deve ter um catchbloco é Load Image User Commandonde o erro é relatado. Idealmente, nada mais deveria ter catchalguma coisa, porque, do contrário, está começando a ser tão entediante e propenso a erros quanto o tratamento de códigos de erro.

Portanto, se você me perguntar, se você tem uma base de código que realmente se beneficia com o tratamento de exceções de uma maneira elegante, ela deve ter o número mínimo de catchblocos (no mínimo, não quero dizer zero, mas mais como um para cada código único operação do usuário final que pode falhar e, possivelmente, menos ainda, se todas as operações de usuário high-end forem chamadas por meio de um sistema de comando central).

Limpeza de Recursos

No entanto, o tratamento de exceções apenas resolve a necessidade de evitar lidar manualmente com os aspectos do fluxo de controle da propagação de erros em caminhos excepcionais separados dos fluxos normais de execução. Muitas vezes, uma função que serve como um propagador de erros, mesmo que faça isso automaticamente agora com o EH, ainda pode adquirir alguns recursos que precisa destruir. Por exemplo, essa função pode abrir um arquivo temporário que precisa ser fechado antes de retornar da função, independentemente do que seja, ou bloquear um mutex necessário para desbloquear, independentemente do que seja.

Para isso, posso invocar a ira de muitos programadores de todos os tipos de linguagens, mas acho que a abordagem C ++ para isso é ideal. A linguagem introduz destruidores que são invocados de maneira determinística no instante em que um objeto sai do escopo. Por esse motivo, o código C ++ que, digamos, bloqueia um mutex por meio de um objeto mutex com escopo definido com um destruidor, não precisa desbloqueá-lo manualmente, pois ele será desbloqueado automaticamente assim que o objeto sair do escopo, aconteça o que acontecer (mesmo se houver uma exceção). encontrado). Portanto, não há realmente nenhuma necessidade de código C ++ bem escrito para lidar com a limpeza de recursos locais.

Em idiomas que não possuem destruidores, eles podem precisar usar um finallybloco para limpar manualmente os recursos locais. Dito isso, ainda é melhor ter que desarrumar seu código com propagação manual de erros, desde que você não tenha catchexceções em todo o lugar.

Reversão de efeitos colaterais externos

Esse é o problema conceitual mais difícil de resolver. Se alguma função, seja um propagador de erros ou um ponto de falha, causar efeitos colaterais externos, será necessário reverter ou "desfazer" esses efeitos colaterais para retornar o sistema a um estado como se a operação nunca tivesse ocorrido, em vez de um " semi-válido "onde a operação foi bem-sucedida. Não conheço linguagens que facilitem muito esse problema conceitual, exceto linguagens que simplesmente reduzem a necessidade de muitas funções causarem efeitos colaterais externos em primeiro lugar, como linguagens funcionais que giram em torno da imutabilidade e estruturas de dados persistentes.

Aqui finallyestá, sem dúvida, uma das soluções mais elegantes disponíveis para o problema nas linguagens que envolvem mutabilidade e efeitos colaterais, porque esse tipo de lógica geralmente é muito específico para uma função específica e não se encaixa tão bem no conceito de "limpeza de recursos" " E eu recomendo usar finallyliberalmente nesses casos para garantir que sua função reverta os efeitos colaterais em idiomas que a suportam, independentemente de você precisar ou não de um catchbloco (e, novamente, se você me perguntar, o código bem escrito deve ter o número mínimo de catchblocos e todos os catchblocos devem estar em locais onde faz mais sentido, como no diagrama acima, em Load Image User Command).

Linguagem dos Sonhos

No entanto, a IMO finallyestá próxima do ideal para a reversão de efeitos colaterais, mas não exatamente. Precisamos introduzir uma booleanvariável para reverter efetivamente os efeitos colaterais no caso de uma saída prematura (de uma exceção lançada ou não), da seguinte forma:

bool finished = false;
try
{
    // Cause external side effects.
    ...

    // Indicate that all the external side effects were
    // made successfully.
    finished = true; 
}
finally
{
    // If the function prematurely exited before finishing
    // causing all of its side effects, whether as a result of
    // an early 'return' statement or an exception, undo the
    // side effects.
    if (!finished)
    {
        // Undo side effects.
        ...
    }
}

Se eu pudesse projetar uma linguagem, minha maneira de sonho de resolver esse problema seria assim para automatizar o código acima:

transaction
{
    // Cause external side effects.
    ...
}
rollback
{
    // This block is only executed if the above 'transaction'
    // block didn't reach its end, either as a result of a premature
    // 'return' or an exception.

    // Undo side effects.
    ...
}

... com destruidores para automatizar a limpeza dos recursos locais, tornando-o necessário transaction, rollbacke catch(embora eu ainda queira adicionar finally, digamos, trabalhar com recursos C que não se limpam). No entanto, finallycom uma booleanvariável é a coisa mais próxima de tornar isso simples que eu descobri até agora sem a linguagem dos meus sonhos. A segunda solução mais direta que encontrei para isso são os protetores de escopo em linguagens como C ++ e D, mas sempre achei protetores de escopo um pouco desajeitados conceitualmente, pois desfocam a idéia de "limpeza de recursos" e "reversão de efeitos colaterais". Na minha opinião, essas são idéias muito distintas para serem abordadas de maneira diferente.

Meu pequeno sonho de uma linguagem também giraria em torno de imutabilidade e estruturas de dados persistentes para facilitar muito, embora não seja necessário, escrever funções eficientes que não precisam copiar profundamente estruturas de dados maciças em sua totalidade, mesmo que a função cause Sem efeitos colaterais.

Conclusão

Enfim, com minhas divagações de lado, acho que seu try/finallycódigo para fechar o soquete é ótimo e ótimo, considerando que o Python não tem o equivalente em C ++ de destruidores, e pessoalmente acho que você deve usá-lo liberalmente em locais que precisam reverter os efeitos colaterais e minimize o número de lugares onde você precisa catchpara lugares onde faz mais sentido.


fonte
-66

Capturar erros / exceção e manipulá-los de maneira organizada é altamente recomendado, mesmo que não seja obrigatório.

A razão pela qual digo isso é porque acredito que todo desenvolvedor deve conhecer e lidar com o comportamento de seu aplicativo, caso contrário ele não concluiu seu trabalho de maneira adequada. Não há situação em que um bloco try-finalmente substitua o bloco try-catch-finally.

Vou dar um exemplo simples: suponha que você tenha escrito o código para fazer upload de arquivos no servidor sem capturar exceções. Agora, se por algum motivo o upload falhar, o cliente nunca saberá o que deu errado. Mas, se você capturou a exceção, pode exibir uma mensagem de erro clara explicando o que deu errado e como o usuário pode remediá-la.

Regra de ouro: sempre pegue uma exceção, porque adivinhar leva tempo

Pankaj Upadhyay
fonte
22
-1: em Java, uma cláusula Finalmente pode ser necessária para liberar recursos (por exemplo, fechar um arquivo ou liberar uma conexão com o banco de dados). Isso é independente da capacidade de lidar com uma exceção.
kevin Cline
@kevincline, Ele não está perguntando se deve usar finalmente ou não ... Tudo o que ele está perguntando é se é necessário ou não capturar uma exceção .... Ele sabe o que tenta, captura e finalmente faz ..... Finalmente é o parte mais essencial, todos nós sabemos que e por que ele é usado ....
Pankaj Upadhyay
63
@Pankaj: sua resposta sugere que uma catchcláusula sempre deve estar presente sempre que houver a try. Contribuidores mais experientes, inclusive eu, acreditam que este é um péssimo conselho. Seu raciocínio é falho. O método que contém o trynão é o único local possível em que a exceção pode ser capturada. Geralmente é mais simples e melhor permitir capturar e relatar exceções do nível mais alto, em vez de duplicar cláusulas de captura em todo o código.
kevin Cline
@ Kevincline, acredito que a percepção da pergunta por parte dos outros é um pouco diferente. A pergunta era precisamente: Devemos pegar uma exceção ou não ... E eu respondi a esse respeito. O tratamento da exceção pode ser feito de várias maneiras e não apenas por tentativa. Mas, essa não era a preocupação do OP. Se levantar uma exceção é bom o suficiente para você e outros caras, devo dizer boa sorte.
Pankaj Upadhyay
Com exceções, você deseja que a execução normal de instruções seja interrompida (e sem verificar manualmente se há êxito em cada etapa). Esse é especialmente o caso de chamadas de método profundamente aninhadas - um método de quatro camadas dentro de uma biblioteca não pode simplesmente "engolir" uma exceção; ele precisa ser jogado de volta por todas as camadas. Obviamente, cada camada pode quebrar a exceção e adicionar informações adicionais; isso geralmente não é feito por várias razões, tempo adicional para desenvolver e verbosidade desnecessária sendo os dois maiores.
Daniel B