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 catch
bloco, 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:
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 NULL
ou 0
ou -1
ou 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 Scanline
talvez seja necessário lidar com uma falha malloc
e retornar um erro para Convert Scanlines
, em seguida, Convert Scanlines
teria que verificar esse erro e passá-lo para Decompress Image
, then Decompress Image->Parse Image
e and Parse Image->Load Image
, e Load Image
para 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 error
tipo 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 catch
blocos em toda a sua base de código. No diagrama acima, o único local que deve ter um catch
bloco é Load Image User Command
onde o erro é relatado. Idealmente, nada mais deveria ter catch
alguma 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 catch
blocos (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 finally
bloco 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 catch
exceçõ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 finally
está, 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 finally
liberalmente 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 catch
bloco (e, novamente, se você me perguntar, o código bem escrito deve ter o número mínimo de catch
blocos e todos os catch
blocos devem estar em locais onde faz mais sentido, como no diagrama acima, em Load Image User Command
).
Linguagem dos Sonhos
No entanto, a IMO finally
está próxima do ideal para a reversão de efeitos colaterais, mas não exatamente. Precisamos introduzir uma boolean
variá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
, rollback
e catch
(embora eu ainda queira adicionar finally
, digamos, trabalhar com recursos C que não se limpam). No entanto, finally
com uma boolean
variá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/finally
có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 catch
para lugares onde faz mais sentido.