Algumas linguagens (como C ++ e versões anteriores do PHP) não suportam a finally
parte de uma try ... catch ... finally
construção. É finally
sempre necessário? Como o código sempre é executado, por que eu não deveria / não colocava esse código depois de um try ... catch
bloco sem uma finally
cláusula? Por que usar um? (Estou procurando uma razão / motivação para usar / não usar finally
, não uma razão para acabar com 'catch' ou porque é legal fazê-lo.)
exception-handling
Agi Hammerthief
fonte
fonte
Respostas:
Além do que outros disseram, também é possível que uma exceção seja lançada dentro da cláusula catch. Considere isto:
Neste exemplo, a
Cleanup()
função nunca é executada, porque uma exceção é lançada na cláusula catch e a próxima captura mais alta na pilha de chamadas captura isso. O uso de um bloco final remove esse risco e torna o código mais limpo para a inicialização.fonte
Como outros já mencionaram, não há garantia de que o código após uma
try
instrução seja executado, a menos que você capture todas as exceções possíveis. Dito isto, isto:pode ser reescrito 1 como:
Mas o último requer que você capture todas as exceções não tratadas, duplique o código de limpeza e lembre-se de lançar novamente. Portanto,
finally
não é necessário , mas é útil .O C ++ não possui
finally
porque Bjarne Stroustrup acredita que o RAII é melhor ou pelo menos é suficiente para a maioria dos casos:1 O código específico para capturar todas as exceções e reproduzir novamente sem perder as informações de rastreamento de pilha varia de acordo com o idioma. Eu usei Java, onde o rastreamento de pilha é capturado quando a exceção é criada. Em C # você usaria
throw;
.fonte
handleError()
segundo caso, não?catch (Throwable t) {}
, com a tentativa .. bloco catch em torno de todo o bloco inicial (para pegar throwables dehandleError
bem)handleErro();
que tornará ainda melhor o argumento sobre por que finalmente os blocos são úteis (mesmo que essa não fosse a pergunta original).finally
, que é muito mais sutil.try
está dentro docatch
para o específico excepção. Em segundo lugar, é possível que você não saiba se pode lidar com o erro com êxito até examinar a exceção ou se a causa da exceção também impede que você lide com o erro (pelo menos nesse nível). Isso é bastante comum ao fazer E / S. O relançamento existe porque a única maneira de garantircleanUp
execuções é capturar tudo , mas o código original permitiria que as exceções originárias docatch (SpecificException e)
bloco se propagassem para cima.finally
Os blocos geralmente são usados para limpar recursos que podem ajudar na legibilidade ao usar várias instruções de retorno:vs
fonte
finally
. (Eu usar o código como no segundo bloco como várias instruções de retorno são desencorajados onde eu trabalho.)Como você aparentemente já supôs, sim, o C ++ fornece os mesmos recursos sem esse mecanismo. Assim, estritamente falando, o mecanismo
try
/finally
não é realmente necessário.Dito isto, ficar sem ele impõe alguns requisitos na maneira como o restante do idioma é projetado. Em C ++, o mesmo conjunto de ações é incorporado no destruidor de uma classe. Isso funciona principalmente (exclusivamente?) Porque a chamada de destruidor em C ++ é determinística. Isso, por sua vez, leva a algumas regras bastante complexas sobre a vida útil dos objetos, algumas das quais são decididamente não intuitivas.
A maioria dos outros idiomas fornece alguma forma de coleta de lixo. Embora existam coisas controversas sobre a coleta de lixo (por exemplo, sua eficiência em relação a outros métodos de gerenciamento de memória), uma coisa geralmente não é: a hora exata em que um objeto será "limpo" pelo coletor de lixo não está diretamente ligada. para o escopo do objeto. Isso evita seu uso quando a limpeza precisa ser determinística, quando é simplesmente necessária para a operação correta, ou quando se lida com recursos tão preciosos que sua limpeza não seja mais retardada arbitrariamente.
try
/finally
fornece uma maneira para esses idiomas lidarem com as situações que exigem essa limpeza determinística.Eu acho que aqueles que afirmam que a sintaxe C ++ para esse recurso é "menos amigável" do que o Java estão perdendo o ponto. Pior, eles estão perdendo um ponto muito mais crucial sobre a divisão de responsabilidades que vai muito além da sintaxe e tem muito mais a ver com a maneira como o código é projetado.
No C ++, essa limpeza determinística acontece no destruidor do objeto. Isso significa que o objeto pode ser (e normalmente deveria ser) projetado para limpar a si próprio. Isso vai para a essência do design orientado a objetos - uma classe deve ser projetada para fornecer uma abstração e impor seus próprios invariantes. No C ++, é preciso exatamente isso - e um dos invariantes que ele fornece é que, quando o objeto é destruído, os recursos controlados por esse objeto (todos eles, não apenas a memória) serão destruídos corretamente.
Java (e similares) são um pouco diferentes. Enquanto eles suportam (meio que) um
finalize
que teoricamente poderia fornecer recursos semelhantes, o suporte é tão fraco que é basicamente inutilizável (e, de fato, essencialmente nunca usado).Como resultado, em vez de a própria classe poder fazer a limpeza necessária, o cliente da classe precisa tomar medidas para fazê-lo. Se fizermos uma comparação suficientemente míope, pode parecer à primeira vista que essa diferença é bastante pequena e o Java é bastante competitivo com o C ++ nesse aspecto. Terminamos com algo assim. No C ++, a classe se parece com isso:
... e o código do cliente se parece com isso:
Em Java, trocamos um pouco mais de código em que o objeto é usado por um pouco menos na classe. Este inicialmente parece com um bonito mesmo trade-off. Na realidade, está longe disso, porque na maioria dos códigos típicos definimos apenas a classe em um lugar, mas a usamos em muitos lugares. A abordagem C ++ significa que apenas escrevemos esse código para lidar com a limpeza em um só lugar. A abordagem Java significa que precisamos escrever esse código para lidar com a limpeza várias vezes, em muitos lugares - em todos os lugares em que usamos um objeto dessa classe.
Resumindo, a abordagem Java basicamente garante que muitas abstrações que tentamos fornecer são "vazadas" - toda e qualquer classe que requer limpeza determinística obriga o cliente da classe a saber sobre os detalhes do que limpar e como fazer a limpeza , em vez de esses detalhes serem ocultos na própria classe.
Embora eu chamei de "a abordagem Java" acima,
try
/finally
e mecanismos semelhantes sob outros nomes não são totalmente restritos ao Java. Para um exemplo de destaque, a maioria (todas?) Das linguagens .NET (por exemplo, C #) fornece o mesmo.As iterações recentes de Java e C # também fornecem algo a meio caminho entre Java "clássico" e C ++ a esse respeito. Em C #, um objeto que deseja automatizar sua limpeza pode implementar a
IDisposable
interface, que fornece umDispose
método que é (pelo menos vagamente) semelhante a um destruidor de C ++. Embora isso possa ser usado via atry
/finally
like em Java, o C # automatiza um pouco mais a tarefa com umausing
instrução que permite definir recursos que serão criados quando um escopo for inserido e destruído quando o escopo for encerrado. Embora ainda esteja muito aquém do nível de automação e certeza fornecido pelo C ++, isso ainda é uma melhoria substancial em relação ao Java. Em particular, o designer de classe pode centralizar os detalhes de comodispor da classe na sua implementação deIDisposable
. Tudo o que resta para o programador do cliente é o menor ônus de escrever umausing
declaração para garantir que aIDisposable
interface seja usada quando deveria. No Java 7 e mais recente, os nomes foram alterados para proteger os culpados, mas a idéia básica é basicamente idêntica.fonte
Não posso acreditar que mais ninguém levantou isso (sem trocadilhos) - você não precisa de uma cláusula de captura !
Isso é perfeitamente razoável:
Nenhuma cláusula catch em qualquer lugar é vista, porque esse método não pode fazer nada útil com essas exceções; eles são deixados para propagar o backup da pilha de chamadas para um manipulador que puder . Capturar e repetir exceções em todos os métodos é uma Má Idéia, especialmente se você está apenas repetindo a mesma exceção. Isso contraria completamente a forma como o Manuseio de Exceções Estruturadas deve funcionar (e está muito perto de retornar um "código de erro" de cada método, apenas na "forma" de uma Exceção).
O que este método não tem que fazer, porém, é para limpar depois de si, de modo que o "mundo exterior" não precisa saber nada sobre a confusão que ele ficou-se em. A cláusula Finalmente faz exatamente isso - não importa como os métodos chamados se comportem, a cláusula Finalmente será executada "na saída" do método (e o mesmo se aplica a todas as cláusulas Finalmente entre o ponto em que a Exceção é lançada e a eventual cláusula catch que lida com isso); cada um é executado como a pilha de chamadas "desenrola".
fonte
O que aconteceria se fosse lançada uma exceção que você não esperava. A tentativa sairia no meio dela e nenhuma cláusula catch será executada.
O bloco finalmente é ajudar com isso e garantir que, independentemente da exceção, a limpeza ocorra.
fonte
finally
, pois você pode evitar exceções "inesperadas" comcatch(Object)
oucatch(...)
catch-alls.Algumas linguagens oferecem construtores e destruidores para seus objetos (por exemplo, C ++, acredito). Com esses idiomas, você pode fazer a maioria (sem dúvida todos) do que geralmente é feito
finally
em um destruidor. Como tal - nessas línguas - umafinally
cláusula pode ser supérflua.Em uma linguagem sem destruidores (por exemplo, Java), é difícil (talvez até impossível) obter a limpeza correta sem a
finally
cláusula. NB - Em Java, existe umfinalise
método, mas não há garantia de que ele será chamado.fonte
finalise
mas eu preferiria não entrar nos argumentos políticos sobre destruidores / finalistas neste momento.finalise
mas com um sabor extensível e um mecanismo semelhante ao oop - muito expressivo e comparável aofinalise
mecanismo de outras linguagens.Tente finalmente e tente pegar são duas coisas diferentes que compartilham apenas a palavra-chave: "tente". Pessoalmente, eu gostaria de ver isso diferente. A razão pela qual você os vê juntos é porque as exceções produzem um "salto".
E o try finalmente foi projetado para executar código, mesmo que o fluxo de programação salte. Seja por uma exceção ou por qualquer outro motivo. É uma maneira elegante de adquirir um recurso e garantir que ele seja limpo depois sem ter que se preocupar com saltos.
fonte
try catch
mas nãotry finally
; código usando o último é convertido em código usando apenas o primeiro, copiando o conteúdo dofinally
bloco em todos os pontos do código em que ele pode precisar ser executado.Como essa pergunta não especifica C ++ como uma linguagem, considerarei uma combinação de C ++ e Java, pois eles adotam uma abordagem diferente para a destruição de objetos, o que está sendo sugerido como uma das alternativas.
Motivos pelos quais você pode usar um bloco finalmente, em vez de codificar após o bloco try-catch
você retorna cedo do bloco try: considere isso
comparado com:
você retorna cedo do (s) bloco (s) de captura: Compare
vs:
Você repete exceções. Comparar:
vs:
Esses exemplos não fazem parecer muito ruins, mas muitas vezes você tem vários desses casos interagindo e mais de um tipo de exceção / recurso em execução.
finally
pode ajudar a impedir que seu código se torne um pesadelo de manutenção emaranhado.Agora, em C ++, eles podem ser manipulados com objetos baseados em escopo. Mas, na IMO, há duas desvantagens nessa abordagem: 1. A sintaxe é menos amigável. 2. A ordem de construção, sendo o inverso da ordem de destruição, pode tornar as coisas menos claras.
Em Java, você não pode conectar o método finalize para fazer sua limpeza, pois não sabe quando isso acontecerá - (bem, você pode, mas esse é um caminho cheio de condições divertidas de corrida - a JVM tem muito escopo para decidir quando destruir coisas - muitas vezes não é quando você espera - mais cedo ou mais tarde do que você poderia esperar - e isso pode mudar à medida que o compilador de hot spot entra em ação ... suspiro ...)
fonte
Tudo o que é lógico "necessário" em uma linguagem de programação são as instruções:
Qualquer algoritmo pode ser implementado usando apenas as instruções acima; todas as outras construções de linguagem existem para facilitar a gravação e a compreensão dos programas por outros programadores.
Consulte o computador antigo para obter hardware real usando um conjunto de instruções tão mínimo.
fonte
Na verdade, a maior diferença para mim geralmente está nos idiomas que suportam,
finally
mas não possuem destruidores, porque você pode modelar toda a lógica associada à "limpeza" (que separarei em duas categorias) através de destruidores em um nível central, sem lidar manualmente com a limpeza lógica em todas as funções relevantes. Quando vejo código C # ou Java fazendo coisas como desbloquear manualmente mutexes e fechar arquivos emfinally
blocos, isso parece desatualizado e mais ou menos como código C quando tudo isso é automatizado em C ++ por meio de destruidores de maneira a libertar os humanos dessa responsabilidade.No entanto, eu ainda acharia uma conveniência leve se o C ++ incluísse
finally
e é porque existem dois tipos de limpeza:O segundo, pelo menos, não é tão intuitivo para a idéia de destruição de recursos, embora você possa fazer isso muito bem com os guardas de escopo que revertem automaticamente as alterações quando são destruídas antes de serem confirmadas. Há
finally
, sem dúvida, fornece pelo menos um pouco (apenas um pouquinho) mecanismo mais simples para o trabalho de guardas de escopo.No entanto, um mecanismo ainda mais direto seria um
rollback
bloco que eu nunca vi em nenhum idioma antes. É meio que um sonho meu, se eu alguma vez desenhei uma linguagem que envolvia o tratamento de exceções. Seria parecido com isto:Essa seria a maneira mais direta de modelar reversões de efeitos colaterais, enquanto os destruidores são praticamente o mecanismo perfeito para a limpeza de recursos locais. Agora, ele salva apenas algumas linhas de código extras da solução de proteção de escopo, mas a razão pela qual eu quero ver uma linguagem com isso é que a reversão do efeito colateral tende a ser o aspecto mais negligenciado (mas mais complicado) do tratamento de exceções em idiomas que giram em torno da mutabilidade. Acho que esse recurso incentivaria os desenvolvedores a pensarem sobre como lidar com exceções da maneira correta em termos de reversão de transações sempre que as funções causarem efeitos colaterais e não forem concluídas e, como um bônus colateral, quando as pessoas perceberem o quão difícil pode ser a reversão adequada, eles podem preferir escrever mais funções sem efeitos colaterais em primeiro lugar.
Há também alguns casos obscuros em que você só deseja fazer coisas diversas, independentemente de sair de uma função, independentemente de como ela saiu, como talvez registrar um registro de data e hora. Não
finally
é sem dúvida a solução mais simples e perfeita para o trabalho, uma vez que tenta instanciar um objeto apenas para usar seu destrutor para o único propósito de registrar um timestamp só se sente realmente estranho (mesmo que você pode fazê-lo muito bem e muito convenientemente com lambdas )fonte
Como muitas outras coisas incomuns sobre a linguagem C ++, a falta de um
try/finally
construto é uma falha de design, se é que você pode chamá-lo assim em uma linguagem que freqüentemente parece não ter feito nenhum trabalho de design real .RAII (o uso da chamada de destruidor determinístico baseado em escopo em objetos baseados em pilha para limpeza) tem duas falhas sérias. A primeira é que requer o uso de objetos baseados em pilha , que são uma abominação que viola o Princípio de Substituição de Liskov. Há muitas boas razões pelas quais nenhuma outra linguagem OO antes ou desde que o C ++ as usou - no epsilon; D não conta, pois se baseia muito em C ++ e não tem participação de mercado de qualquer maneira - e explicar os problemas que eles causam está além do escopo desta resposta.
Segundo, o que se
finally
pode fazer é um superconjunto de destruição de objetos. Muito do que é feito com RAII em C ++ seria descrito na linguagem Delphi, que não possui coleta de lixo, com o seguinte padrão:Este é o padrão RAII explicitado; se você criar uma rotina C ++ que contenha apenas o equivalente à primeira e terceira linhas acima, o que o compilador geraria acabaria parecido com o que escrevi em sua estrutura básica. E como é o único acesso à
try/finally
construção que o C ++ fornece, os desenvolvedores de C ++ acabam com uma visão bastante míopetry/finally
: quando tudo que você tem é um martelo, tudo começa a parecer um destruidor, por assim dizer.Mas há outras coisas que um desenvolvedor experiente pode fazer com uma
finally
construção. Não se trata de destruição determinística, mesmo diante de uma exceção que está sendo levantada; trata-se de execução de código determinístico , mesmo em caso de exceção.Aqui está outra coisa que você normalmente vê no código Delphi: Um objeto de conjunto de dados com controles de usuário vinculados a ele. O conjunto de dados contém dados de uma fonte externa e os controles refletem o estado dos dados. Se você estiver prestes a carregar um monte de dados no seu conjunto de dados, desabilitará temporariamente a ligação de dados para que não faça coisas estranhas à sua interface do usuário, tentando atualizá-lo várias vezes a cada novo registro inserido. , então você codificaria assim:
Claramente, não há nenhum objeto sendo destruído aqui, nem a necessidade de um. O código é simples, conciso, explícito e eficiente.
Como isso seria feito em C ++? Bem, primeiro você teria que codificar uma classe inteira . Provavelmente seria chamado
DatasetEnabler
ou algo assim. Toda a sua existência seria como um ajudante da RAII. Então você precisaria fazer algo assim:Sim, esses chavetas aparentemente supérfluas são necessárias para gerenciar o escopo adequado e garantir que o conjunto de dados seja reativado imediatamente e não no final do método. Portanto, o que você acaba usando não exige menos linhas de código (a menos que você use chaves egípcias). Requer a criação de um objeto supérfluo, com sobrecarga. (O código C ++ não deve ser rápido?) Não é explícito, mas depende da mágica do compilador. O código que é executado não é descrito em nenhum lugar neste método, mas reside em uma classe totalmente diferente, possivelmente em um arquivo totalmente diferente . Em suma, não é de forma alguma uma solução melhor do que ser capaz de escrever o
try/finally
bloco você mesmo.Esse tipo de problema é bastante comum no design de linguagem e existe um nome para ele: inversão de abstração. Ocorre quando uma construção de alto nível é construída sobre uma construção de baixo nível e, em seguida, a construção de baixo nível não é diretamente suportada no idioma, exigindo que aqueles que desejam usá-lo o reimplemente em termos de construção de alto nível, muitas vezes sujeita a penalidades acentuadas à legibilidade e à eficiência do código.
fonte