Manipulação de exceção - Exception handling

Na computação e na programação de computadores , o tratamento de exceções é o processo de responder à ocorrência de exceções - condições anômalas ou excepcionais que requerem processamento especial - durante a execução de um programa . Em geral, uma exceção interrompe o fluxo normal de execução e executa um manipulador de exceção pré-registrado ; os detalhes de como isso é feito dependem se é uma exceção de hardware ou software e como a exceção de software é implementada. O tratamento de exceções, se fornecido, é facilitado por linguagem de programação especializadaconstruções, mecanismos de hardware como interrupções ou recursos de comunicação entre processos (IPC) do sistema operacional (SO) , como sinais . Algumas exceções, especialmente as de hardware, podem ser tratadas de maneira tão elegante que a execução pode continuar de onde foi interrompida.

Uma abordagem alternativa para manipulação de exceção no software é a verificação de erros, o que mantém o fluxo normal do programa com posteriores verificações explícitas para contingências relataram o uso de especiais de retorno valores, um auxiliar variável global , como C ' s errno , ou sinalizadores de status de ponto flutuante. A validação de entrada , que filtra preventivamente casos excepcionais, também é uma abordagem.

Em hardware

Os mecanismos de exceção de hardware são processados ​​pela CPU. Destina-se a oferecer suporte, por exemplo, detecção de erros e redireciona o fluxo do programa para rotinas de serviço de tratamento de erros. O estado antes da exceção é salvo, por exemplo, na pilha.

Tratamento / armadilhas de exceção de hardware: ponto flutuante IEEE 754

O tratamento de exceções no padrão de hardware de ponto flutuante IEEE 754 refere-se em geral a condições excepcionais e define uma exceção como "um evento que ocorre quando uma operação em alguns operandos específicos não tem um resultado adequado para cada aplicação razoável. Essa operação pode sinalizar uma ou mais exceções invocando o padrão ou, se explicitamente solicitado, um tratamento alternativo definido por idioma. "

Por padrão, uma exceção IEEE 754 pode ser retomada e tratada substituindo-se por um valor predefinido para diferentes exceções, por exemplo, infinito para uma exceção de divisão por zero e fornecendo sinalizadores de status para verificação posterior se a exceção ocorreu (consulte a linguagem de programação C99 para um típico exemplo de tratamento de exceções IEEE 754). Um estilo de tratamento de exceção habilitado pelo uso de sinalizadores de status envolve: primeiro computar uma expressão usando uma implementação rápida e direta; verificar se falhou testando sinalizadores de status; e então, se necessário, chamar uma implementação mais lenta e numericamente mais robusta.

O padrão IEEE 754 usa o termo "trapping" para se referir à chamada de uma rotina de tratamento de exceções fornecida pelo usuário em condições excepcionais e é um recurso opcional do padrão. O padrão recomenda vários cenários de uso para isso, incluindo a implementação de pré-substituição não padrão de um valor seguida de retomada, para lidar de forma concisa com singularidades removíveis .

O comportamento padrão de tratamento de exceções IEEE 754 de retomada após a pré-substituição de um valor padrão evita os riscos inerentes à mudança do fluxo de controle do programa em exceções numéricas. Por exemplo, em 1996, o vôo inaugural do Ariane 5 (vôo 501) terminou em uma explosão catastrófica devido em parte à política de manipulação de exceção da linguagem de programação Ada de abortar a computação em erro aritmético, que neste caso era um ponto flutuante de 64 bits para estouro de conversão de inteiro de 16 bits . No caso do Ariane Flight 501, os programadores protegeram apenas quatro das sete variáveis ​​críticas contra o estouro devido a preocupações sobre as restrições computacionais do computador de bordo e confiaram no que acabou sendo suposições incorretas sobre a possível faixa de valores para o três variáveis ​​desprotegidas porque reutilizaram o código do Ariane 4, para o qual suas suposições estavam corretas. De acordo com William Kahan , a perda do voo 501 teria sido evitada se a política de tratamento de exceções IEEE 754 de substituição padrão tivesse sido usada porque o estouro de conversão de 64 bits para 16 bits que causou o aborto do software ocorreu em um pedaço de código que se revelou completamente desnecessário no Ariane 5. O relatório oficial sobre o acidente (conduzido por um comitê de investigação liderado por Jacques-Louis Lions ) observou que "Um tema subjacente no desenvolvimento do Ariane 5 é o viés para a mitigação de falha aleatória . O fornecedor do sistema de navegação inercial (SRI) estava apenas seguindo a especificação que lhe era dada, que estipulava que caso fosse detectada alguma exceção o processador deveria ser parado. A exceção ocorrida não se deveu a falha aleatória mas um erro de projeto. A exceção foi detectada, mas tratada de forma inadequada porque a visão foi tomada de que o software deve ser considerado correto até que seja demonstrado que a falha é [...] h a falha foi devida a um erro sistemático de projeto de software, mecanismos podem ser introduzidos para mitigar esse tipo de problema. Por exemplo, os computadores nos SRIs podem ter continuado a fornecer suas melhores estimativas das informações de atitude exigidas . Há motivos para preocupação de que uma exceção de software deva ser permitida, ou mesmo exigida, para fazer com que um processador pare durante o manuseio de equipamento de missão crítica. Na verdade, a perda de uma função de software adequada é perigosa porque o mesmo software é executado em ambas as unidades SRI. No caso do Ariane 501, isso resultou no desligamento de duas unidades críticas de equipamentos ainda saudáveis. "

Do ponto de vista do processamento, as interrupções de hardware são semelhantes às exceções retomáveis, embora normalmente não estejam relacionadas ao fluxo de controle do programa do usuário .

Recursos de tratamento de exceções fornecidos pelo sistema operacional

Os sistemas operacionais do tipo Unix fornecem recursos para lidar com exceções em programas via IPC . Normalmente, as interrupções causadas pela execução de um processo são tratadas pelas rotinas de serviço de interrupção do sistema operacional, e o sistema operacional pode então enviar um sinal para esse processo, que pode ter solicitado ao sistema operacional para registrar um manipulador de sinal a ser chamado quando o sinal é gerado, ou deixe o sistema operacional executar uma ação padrão (como encerrar o programa). Exemplos típicos são SIGSEGV , SIGBUS , SIGILL e SIGFPE .

Outros sistemas operacionais, por exemplo, OS / 360 e sucessores , podem usar abordagens diferentes no lugar ou além do IPC.

Em software

O tratamento de exceções de software e o suporte fornecido por ferramentas de software diferem um pouco do que é entendido por tratamento de exceções em hardware, mas conceitos semelhantes estão envolvidos. Em mecanismos de linguagem de programação para tratamento de exceção, o termo exceção é normalmente usado em um sentido específico para denotar uma estrutura de dados que armazena informações sobre uma condição excepcional. Um mecanismo para transferir o controle ou gerar uma exceção é conhecido como lançamento . Diz-se que a exceção foi lançada . A execução é transferida para um "catch".

Do ponto de vista do autor de uma rotina , lançar uma exceção é uma forma útil de sinalizar que uma rotina não pode ser executada normalmente - por exemplo, quando um argumento de entrada é inválido (por exemplo, o valor está fora do domínio de uma função ) ou quando um recurso do qual depende não está disponível (como um arquivo ausente, um erro de disco rígido ou erros de falta de memória), ou que a rotina detectou uma condição normal que requer tratamento especial, por exemplo, atenção, fim do arquivo . Em sistemas sem exceções, as rotinas precisariam retornar algum código de erro especial . No entanto, isso às vezes é complicado pelo problema semipredicado , no qual os usuários da rotina precisam escrever código extra para distinguir os valores de retorno normais dos errôneos.

As linguagens de programação diferem substancialmente em sua noção do que é uma exceção. As línguas contemporâneas podem ser divididas em dois grupos:

  • Linguagens onde as exceções são projetadas para serem usadas como estruturas de controle de fluxo: Ada, Modula-3, ML, OCaml, PL / I, Python e Ruby se enquadram nesta categoria.
  • Linguagens em que as exceções são usadas apenas para lidar com situações anormais, imprevisíveis e errôneas: C ++, Java, C #, Common Lisp, Eiffel e Modula-2.

Kiniry também observa que "O design da linguagem influencia apenas parcialmente o uso de exceções e, consequentemente, a maneira como se lida com falhas parciais e totais durante a execução do sistema. A outra grande influência são os exemplos de uso, normalmente em bibliotecas centrais e exemplos de código em técnicas livros, artigos de revistas e fóruns de discussão online e nos padrões de código de uma organização. "

Os aplicativos contemporâneos enfrentam muitos desafios de design ao considerar estratégias de tratamento de exceções. Particularmente em aplicativos modernos de nível empresarial, as exceções devem frequentemente cruzar os limites do processo e os limites da máquina. Parte do projeto de uma estratégia sólida de tratamento de exceções é reconhecer quando um processo falhou a ponto de não poder ser tratado economicamente pela parte de software do processo.

História

Tratamento de exceções de software desenvolvido em Lisp nas décadas de 1960 e 1970. Originou-se no LISP 1.5 (1962), onde as exceções eram capturadas pela ERRSETpalavra - chave, que retornava NILem caso de erro, ao invés de encerrar o programa ou entrar no depurador. O levantamento de erros foi introduzido no MacLisp no final dos anos 1960 por meio da ERRpalavra - chave. Isso foi rapidamente usado não apenas para aumento de erro, mas para fluxo de controle não local e, portanto, foi aumentado por duas novas palavras-chave CATCHe THROW(MacLisp junho de 1972), reserving ERRSETe ERRpara tratamento de erros. O comportamento de limpeza agora geralmente chamado de "finalmente" foi introduzido no NIL (Nova Implementação do LISP) em meados da década de 1970 como UNWIND-PROTECT. Isso foi então adotado pelo Common Lisp . Contemporâneo a isso estava dynamic-windem Scheme, que lidava com exceções em fechamentos. Os primeiros artigos sobre tratamento estruturado de exceções foram Goodenough (1975a) e Goodenough (1975b) . O tratamento de exceções foi subsequentemente amplamente adotado por muitas linguagens de programação a partir da década de 1980 em diante.

PL / I usou exceções com escopo dinâmico, porém as linguagens mais recentes usam exceções com escopo léxico. O tratamento de exceções PL / I inclui eventos que não são erros, por exemplo, atenção, fim do arquivo, modificação das variáveis ​​listadas. Embora algumas linguagens mais recentes suportem exceções sem erros, seu uso não é comum.

Originalmente, o tratamento de exceções de software incluía exceções retomáveis ​​(semântica de retomada), como a maioria das exceções de hardware, e exceções não retomáveis ​​(semântica de terminação). No entanto, a semântica de retomada foi considerada ineficaz na prática nas décadas de 1970 e 1980 (consulte a discussão sobre padronização C ++, citada abaixo) e não é mais comum, embora fornecida por linguagens de programação como Common Lisp, Dylan e PL / I.

Semântica de rescisão

Os mecanismos de tratamento de exceções em linguagens contemporâneas são normalmente não recuperáveis ​​("semântica de terminação") em oposição às exceções de hardware, que normalmente são recuperáveis. Isso se baseia na experiência de usar ambos, pois há argumentos teóricos e de design a favor de qualquer uma das decisões; estes foram amplamente debatidos durante as discussões de padronização C ++ 1989-1991, o que resultou em uma decisão definitiva para a semântica de terminação. Sobre a justificativa para tal design para o mecanismo C ++, Stroustrup observa:

[N] a reunião de Palo Alto [padronização C ++] em novembro de 1991, ouvimos um resumo brilhante dos argumentos para a semântica de rescisão apoiada tanto em experiência pessoal quanto em dados de Jim Mitchell (da Sun, anteriormente da Xerox PARC). Jim usou o tratamento de exceções em meia dúzia de idiomas por um período de 20 anos e foi um dos primeiros defensores da semântica de retomada como um dos principais designers e implementadores do sistema Cedar / Mesa da Xerox . Sua mensagem foi

“A rescisão é preferível à retomada; isto não é uma questão de opinião, mas sim de anos de experiência. A retomada é sedutora, mas não é válida. ”

Ele apoiou esta declaração com experiência em vários sistemas operacionais. O exemplo chave foi Cedar / Mesa: foi escrito por pessoas que gostaram e usaram retomada, mas depois de dez anos de uso, havia apenas um uso de retomada restante no sistema de meio milhão de linhas - que era uma investigação de contexto. Como a retomada não era realmente necessária para tal investigação de contexto, eles a removeram e encontraram um aumento significativo de velocidade nessa parte do sistema. Em todos os casos em que a retomada foi usada, ela - ao longo dos dez anos - se tornou um problema e um projeto mais apropriado o substituiu. Basicamente, todo uso de retomada representou uma falha em manter níveis separados de abstração separados.

Crítica

Uma visão contrastante sobre a segurança do tratamento de exceções foi dada por Tony Hoare em 1980, descrevendo a linguagem de programação Ada como tendo "... uma infinidade de recursos e convenções de notação, muitos deles desnecessários e alguns deles, como tratamento de exceções, até perigoso. [...] Não permita que esta linguagem em seu estado atual seja usada em aplicações onde a confiabilidade é crítica [...]. O próximo foguete a se perder como resultado de um erro de linguagem de programação pode não ser exploratório foguete espacial em uma viagem inofensiva a Vênus: pode ser uma ogiva nuclear explodindo sobre uma de nossas próprias cidades. "

O tratamento de exceções geralmente não é tratado corretamente no software, especialmente quando há várias fontes de exceções; a análise do fluxo de dados de 5 milhões de linhas de código Java encontrou mais de 1300 defeitos de manipulação de exceção. Citando vários estudos anteriores de outros (1999–2004) e seus próprios resultados, Weimer e Necula escreveram que um problema significativo com exceções é que elas "criam caminhos de fluxo de controle ocultos que são difíceis para os programadores raciocinarem sobre".

Go foi inicialmente lançado com o tratamento de exceções explicitamente omitido, com os desenvolvedores argumentando que ofuscou o fluxo de controle . Posteriormente, o mecanismo panic/ tipo exceção recoverfoi adicionado à linguagem, que os autores do Go aconselham o uso apenas para erros irrecuperáveis ​​que devem interromper todo o processo.

Exceções, como fluxo não estruturado, aumentam o risco de vazamentos de recursos (como escapar de uma seção bloqueada por um mutex ou manter um arquivo aberto temporariamente) ou estado inconsistente. Existem várias técnicas para gerenciamento de recursos na presença de exceções, mais comumente combinando o padrão de descarte com alguma forma de proteção de desenrolamento (como uma finallycláusula), que libera automaticamente o recurso quando o controle sai de uma seção do código.

Suporte de exceção em linguagens de programação

Muitas linguagens de computador possuem suporte integrado para exceções e tratamento de exceções. Isso inclui ActionScript , Ada , BlitzMax , C ++ , C # , Clojure , COBOL , D , ECMAScript , Eiffel , Java , ML , Next Generation Shell , Object Pascal (por exemplo , Delphi , Free Pascal e semelhantes), PowerBuilder , Objective-C , OCaml , PHP (a partir da versão 5), PL / I , PL / SQL , Prolog , Python , REALbasic , Ruby , Scala , Seed7 , Smalltalk , Tcl , Visual Prolog e a maioria das linguagens .NET . O tratamento de exceções normalmente não pode ser retomado nessas linguagens e, quando uma exceção é lançada, o programa pesquisa de volta na pilha de chamadas de função até que um manipulador de exceção seja encontrado.

Algumas linguagens exigem o desenrolar da pilha à medida que a pesquisa avança. Isto é, se a função F , contendo um manipulador de H para excepção E , chama a função g , que por sua vez solicita a função h , e uma excepção E ocorre em h , em seguida, as funções de h e g podem ser terminada, e H em f irá lidar com E .

As linguagens de tratamento de exceções sem esse desdobramento são Common Lisp com seu Condition System , PL / I e Smalltalk . Todos chamam o manipulador de exceções e não desenrolam a pilha; entretanto, em PL / I, se a "unidade ON" (manipulador de exceção) fizer um GOTO fora da unidade ON, isso irá desenrolar a pilha. O manipulador de exceções tem a opção de reiniciar a computação, retomar ou desenrolar. Isso permite que o programa continue a computação exatamente no mesmo lugar onde o erro ocorreu (por exemplo, quando um arquivo anteriormente ausente se tornou disponível) ou para implementar notificações, registro, consultas e variáveis ​​fluidas no topo do mecanismo de tratamento de exceção (como feito em Smalltalk). A implementação sem pilha da linguagem de programação Mythryl oferece suporte ao tratamento de exceções em tempo constante sem desenrolar da pilha.

Excluindo pequenas diferenças sintáticas, há apenas alguns estilos de tratamento de exceção em uso. No estilo mais popular, uma exceção é iniciada por uma instrução especial ( throwou raise) com um objeto de exceção (por exemplo, com Java ou Object Pascal) ou um valor de um tipo enumerado extensível especial (por exemplo, com Ada ou SML). O espaço para manipuladores de exceção começa com uma cláusula marcador ( tryou arranque bloco da linguagem como begin) e termina no início da primeira cláusula manipulador ( catch, except, rescue). Várias cláusulas do manipulador podem seguir e cada uma pode especificar quais tipos de exceção ela trata e qual nome ela usa para o objeto de exceção.

Algumas linguagens também permitem uma cláusula ( else) que é usada no caso de nenhuma exceção ocorrer antes de o fim do escopo do manipulador ser atingido.

Mais comum é uma cláusula relacionada ( finallyou ensure) que é executada independentemente de ter ocorrido uma exceção ou não, normalmente para liberar recursos adquiridos dentro do corpo do bloco de tratamento de exceção. Notavelmente, C ++ não fornece essa construção, uma vez que incentiva a técnica RAII ( Resource Acquisition Is Initialization ), que libera recursos usando destruidores .

De maneira geral, o código de tratamento de exceções pode ter a seguinte aparência (em pseudocódigo semelhante ao Java ):

try {
    line = console.readLine();

    if (line.length() == 0) {
        throw new EmptyLineException("The line read from console was empty!");
    }

    console.printLine("Hello %s!" % line);
    console.printLine("The program ran successfully.");
}
catch (EmptyLineException e) {
    console.printLine("Hello!");
}
catch (Exception e) {
    console.printLine("Error: " + e.message());
}
finally {
    console.printLine("The program is now terminating.");
}

Como uma variação menor, algumas linguagens usam uma única cláusula de tratamento, que lida com a classe da exceção internamente.

De acordo com um artigo de 2008 de Westley Weimer e George Necula , a sintaxe dos try... finallyblocos em Java é um fator que contribui para defeitos de software. Quando um método precisa lidar com a aquisição e liberação de 3–5 recursos, os programadores aparentemente não desejam aninhar blocos suficientes devido a questões de legibilidade, mesmo quando esta seria uma solução correta. É possível usar um único bloco try... finallymesmo quando se trata de vários recursos, mas isso requer um uso correto dos valores sentinela , que é outra fonte comum de bugs para este tipo de problema. Em relação à semântica do try... catch... finallyconstrução em geral, Weimer e Necula escrevem que "Embora try-catch-finally seja conceitualmente simples, tem a descrição de execução mais complicada na especificação da linguagem [Gosling et al. 1996] e requer quatro níveis de “if” s aninhados em sua descrição oficial em inglês. Em suma, contém um grande número de casos extremos que os programadores costumam ignorar. "

C oferece suporte a vários meios de verificação de erros, mas geralmente não é considerado como suporte para "tratamento de exceções", embora as funções da biblioteca padrão setjmpelongjmp possam ser usadas para implementar a semântica de exceção.

Perl tem suporte opcional para tratamento estruturado de exceções.

O suporte do Python para tratamento de exceções é abrangente e consistente. É difícil escrever um programa Python robusto sem usar suas palavras try- exceptchave e .

Tratamento de exceções em hierarquias de IU

Estruturas web front-end recentes, como React e Vue , introduziram mecanismos de tratamento de erros em que os erros se propagam pela hierarquia do componente de IU, de uma forma análoga a como os erros se propagam pela pilha de chamadas ao executar o código. Aqui, o mecanismo de limite de erro serve como um análogo ao mecanismo típico de try-catch. Portanto, um componente pode garantir que os erros de seus componentes filhos sejam detectados e manipulados, e não propagados para os componentes pais.

Por exemplo, no Vue, um componente detectaria erros ao implementar errorCaptured

Vue.component('parent', {
    template: '<div><slot></slot></div>',
    errorCaptured: (err, vm, info) => alert('An error occurred');
})
Vue.component('child', {
    template: '<div>{{ cause_error() }}</div>'
})

Quando usado assim na marcação:

<parent>
    <child></child>
</parent>

O erro produzido pelo componente filho é detectado e tratado pelo componente pai.

Implementação de tratamento de exceções

A implementação do tratamento de exceções em linguagens de programação normalmente envolve uma boa quantidade de suporte de um gerador de código e do sistema de tempo de execução que acompanha um compilador. (Foi a adição do tratamento de exceções ao C ++ que encerrou a vida útil do compilador C ++ original, Cfront .) Dois esquemas são mais comuns. O primeiro, o registro dinâmico , gera um código que atualiza continuamente as estruturas sobre o estado do programa em termos de tratamento de exceções. Normalmente, isso adiciona um novo elemento ao layout do quadro de pilha que sabe quais manipuladores estão disponíveis para a função ou método associado a esse quadro; se uma exceção é lançada, um ponteiro no layout direciona o tempo de execução para o código do manipulador apropriado. Essa abordagem é compacta em termos de espaço, mas adiciona sobrecarga de execução na entrada e saída de quadros. Era comumente usado em muitas implementações Ada, por exemplo, onde geração complexa e suporte de tempo de execução já eram necessários para muitos outros recursos de linguagem. O registro dinâmico, sendo bastante simples de definir, é passível de prova de correção .

O segundo esquema, e aquele implementado em muitos compiladores C ++ com qualidade de produção, é uma abordagem baseada em tabelas . Isso cria tabelas estáticas em tempo de compilação e tempo de link que relacionam intervalos do contador do programa ao estado do programa com respeito ao tratamento de exceções. Então, se uma exceção é lançada, o sistema de tempo de execução procura a localização da instrução atual nas tabelas e determina quais manipuladores estão em jogo e o que precisa ser feito. Essa abordagem minimiza a sobrecarga executiva para o caso em que uma exceção não é lançada. Isso acontece ao custo de algum espaço, mas esse espaço pode ser alocado em seções de dados de propósito especial somente leitura que não são carregadas ou realocadas até que uma exceção seja realmente lançada. Esta segunda abordagem também é superior em termos de segurança de rosca .

Outros esquemas de definição e implementação também foram propostos. Para linguagens que suportam metaprogramação , abordagens que não envolvem sobrecarga (além do suporte já presente para reflexão ) foram avançadas.

Tratamento de exceções com base no projeto por contrato

Uma visão diferente das exceções é baseada nos princípios de design por contrato e é apoiada em particular pela linguagem de Eiffel . A ideia é fornecer uma base mais rigorosa para o tratamento de exceções, definindo precisamente o que é comportamento "normal" e "anormal". Especificamente, a abordagem é baseada em dois conceitos:

  • Falha : incapacidade de uma operação de cumprir seu contrato. Por exemplo, uma adição pode produzir um estouro aritmético (não cumpre seu contrato de calcular uma boa aproximação da soma matemática); ou uma rotina pode falhar em atender sua pós-condição.
  • Exceção : um evento anormal que ocorre durante a execução de uma rotina (essa rotina é o " destinatário " da exceção) durante sua execução. Esse evento anormal resulta da falha de uma operação chamada pela rotina.

O "princípio de Manipulação de Exceção Segura", introduzido por Bertrand Meyer na Construção de Software Orientada a Objetos , afirma que há apenas duas maneiras significativas de uma rotina reagir quando ocorre uma exceção:

  • Falha, ou "pânico organizado": a rotina corrige o estado do objeto ao restabelecer o invariante (esta é a parte "organizada") e, em seguida, falha (pânico), disparando uma exceção em seu chamador (para que o evento anormal seja não ignorado).
  • Repetir: A rotina tenta o algoritmo novamente, geralmente após alterar alguns valores para que a próxima tentativa tenha uma chance melhor de sucesso.

Em particular, simplesmente ignorar uma exceção não é permitido; um bloco deve ser repetido e concluído com êxito ou propagar a exceção para seu chamador.

Aqui está um exemplo expresso na sintaxe de Eiffel. Ele assume que uma rotina send_fasté normalmente a melhor maneira de enviar uma mensagem, mas pode falhar, disparando uma exceção; em caso afirmativo, o algoritmo a seguir usa send_slow, que falhará com menos frequência. Se send_slowfalhar, a rotina sendcomo um todo deve falhar, fazendo com que o chamador obtenha uma exceção.

send (m: MESSAGE) is
  -- Send m through fast link, if possible, otherwise through slow link.
local
  tried_fast, tried_slow: BOOLEAN
do
  if tried_fast then
     tried_slow := True
     send_slow (m)
  else
     tried_fast := True
     send_fast (m)
  end
rescue
  if not tried_slow then
     retry
  end
end

As variáveis ​​locais booleanas são inicializadas como False no início. Se send_fastfalhar, o corpo ( docláusula) será executado novamente, causando a execução de send_slow. Se esta execução send_slowfalhar, a rescuecláusula será executada até o fim sem retry(nenhuma elsecláusula no final if), fazendo com que a execução da rotina como um todo falhe.

Essa abordagem tem o mérito de definir claramente o que são casos "normais" e "anormais": um caso anormal, causando uma exceção, é aquele em que a rotina não consegue cumprir seu contrato. Ele define uma distribuição clara de papéis: a docláusula (corpo normal) é responsável por cumprir, ou tentar cumprir, o contrato da rotina; a rescuecláusula é responsável por restabelecer o contexto e reiniciar o processo, se houver chance de sucesso, mas não de realizar nenhum cálculo real.

Embora as exceções em Eiffel tenham uma filosofia bastante clara, Kiniry (2006) critica sua implementação porque "As exceções que fazem parte da definição da linguagem são representadas por valores INTEGER, exceções definidas pelo desenvolvedor por valores STRING. [...] Além disso, porque eles são valores básicos e não objetos, eles não têm semântica inerente além daquela que é expressa em uma rotina auxiliar que necessariamente não pode ser infalível por causa da sobrecarga de representação em vigor (por exemplo, não se pode diferenciar dois inteiros do mesmo valor). "

Exceções não capturadas

Se uma exceção é lançada e não capturada (operacionalmente, uma exceção é lançada quando não há nenhum manipulador aplicável especificado), a exceção não capturada é tratada pelo tempo de execução; a rotina que faz isso é chamada de manipulador de exceção não capturada . O comportamento padrão mais comum é encerrar o programa e imprimir uma mensagem de erro no console, geralmente incluindo informações de depuração, como uma representação de string da exceção e orastreamento de pilha. Isso geralmente é evitado por ter um manipulador de nível superior (nível de aplicativo) (por exemplo, em umloop de evento) que captura as exceções antes que elas atinjam o tempo de execução.

Nota que, apesar de uma exceção não pega pode resultar no programa terminar de forma anormal (o programa pode não ser correta se uma exceção não é detectada, nomeadamente, por não rolar transações volta parcialmente concluída, ou não liberar recursos), o processo termina normalmente (assumindo que o o tempo de execução funciona corretamente), pois o tempo de execução (que controla a execução do programa) pode garantir o desligamento ordenado do processo.

Em um programa multithread, uma exceção não capturada em um thread pode, em vez disso, resultar no encerramento apenas desse thread, não de todo o processo (exceções não capturadas no manipulador de nível de thread são capturadas pelo manipulador de nível superior). Isso é particularmente importante para servidores, onde, por exemplo, um servlet (em execução em seu próprio encadeamento) pode ser encerrado sem que o servidor como um todo seja afetado.

Este manipulador de exceção não capturada padrão pode ser substituído, globalmente ou por thread, por exemplo, para fornecer log alternativo ou relatório do usuário final de exceções não capturadas, ou para reiniciar threads que terminam devido a uma exceção não capturada. Por exemplo, em Java, isso é feito para um único thread via Thread.setUncaughtExceptionHandlere globalmente via Thread.setDefaultUncaughtExceptionHandler; em Python, isso é feito modificando sys.excepthook.

Verificação estática de exceções

Exceções verificadas

Os designers de Java criaram exceções verificadas, que são um conjunto especial de exceções. As exceções verificadas que um método pode gerar fazem parte da assinatura do método . Por exemplo, se um método pode lançar um IOException, ele deve declarar esse fato explicitamente em sua assinatura de método. A falha em fazer isso gera um erro em tempo de compilação.

Kiniry (2006) observa, no entanto, que as bibliotecas Java (como eram em 2006) eram frequentemente inconsistentes em sua abordagem de relatório de erros, porque "No entanto, nem todas as situações erradas em Java são representadas por exceções. Muitos métodos retornam valores especiais que indicam falha codificada como campo constante de classes relacionadas. "

As exceções verificadas estão relacionadas aos verificadores de exceção que existem para a linguagem de programação OCaml . A ferramenta externa para OCaml é invisível (ou seja, não requer nenhuma anotação sintática) e opcional (ou seja, é possível compilar e executar um programa sem ter verificado as exceções, embora isso não seja recomendado para código de produção).

A linguagem de programação CLU tinha um recurso com a interface mais próxima do que o Java introduziu posteriormente. Uma função poderia gerar apenas exceções listadas em seu tipo, mas quaisquer exceções de vazamento de funções chamadas seriam automaticamente transformadas na única exceção de tempo de execução failure, em vez de resultar em erro de tempo de compilação. Mais tarde, o Modula-3 teve um recurso semelhante. Esses recursos não incluem a verificação do tempo de compilação, que é central no conceito de exceções verificadas, e não foi (até 2006) incorporada nas principais linguagens de programação além de Java.

As primeiras versões da linguagem de programação C ++ incluíam um mecanismo opcional para exceções verificadas, chamado especificações de exceção . Por padrão, qualquer função pode lançar qualquer exceção, mas isso pode ser limitado por uma cláusula adicionada à assinatura da função, que especifica quais exceções a função pode lançar. As especificações de exceção não foram aplicadas em tempo de compilação. As violações resultaram na chamada da função global . Uma especificação de exceção vazia pode ser fornecida, o que indica que a função não lançará nenhuma exceção. Isso não se tornou o padrão quando o tratamento de exceções foi adicionado à linguagem porque teria exigido muitas modificações do código existente, teria impedido a interação com o código escrito em outras linguagens e teria tentado os programadores a escrever muitos manipuladores no local nível. O uso explícito de especificações de exceção vazias pode, entretanto, permitir que compiladores C ++ executem código significativo e otimizações de layout de pilha que são impedidas quando o tratamento de exceção pode ocorrer em uma função. Alguns analistas consideraram difícil o uso adequado de especificações de exceção em C ++. Esse uso de especificações de exceção foi incluído no C ++ 03 , obsoleto no padrão de linguagem C ++ de 2012 ( C ++ 11 ) e foi removido da linguagem C ++ 17 . Uma função que não lançará nenhuma exceção agora pode ser denotada pela palavra - chave. throwstd::unexpectednoexcept

Em contraste com Java, linguagens como C # não requerem declaração de nenhum tipo de exceção. De acordo com Hanspeter Mössenböck, não distinguir entre exceções a serem chamadas (verificadas) e exceções não chamadas (não verificadas) torna o programa escrito mais conveniente, mas menos robusto, pois uma exceção não capturada resulta em um aborto com um rastreamento de pilha . Kiniry (2006) observa, no entanto, que o JDK do Java (versão 1.4.1) lança um grande número de exceções não verificadas: uma para cada 140 linhas de código, enquanto Eiffel as usa com muito mais moderação, com uma lançada a cada 4.600 linhas de código. Kiniry também escreve que "Como qualquer programador Java sabe, o volume de try catchcódigo em um aplicativo Java típico é às vezes maior do que o código comparável necessário para parâmetro formal explícito e verificação de valor de retorno em outras linguagens que não têm exceções verificadas. O consenso geral entre os programadores Java das trincheiras é que lidar com exceções verificadas é uma tarefa quase tão desagradável quanto escrever documentação. Assim, muitos programadores relatam que "reenviam" exceções verificadas. Isso leva a uma abundância de exceções verificadas, mas ignoradas. exceções ". Kiniry também observa que os desenvolvedores de C # aparentemente foram influenciados por esse tipo de experiência do usuário, com a seguinte citação sendo atribuída a eles (via Eric Gunnerson):

"O exame de pequenos programas leva à conclusão de que exigir especificações de exceção pode aumentar a produtividade do desenvolvedor e melhorar a qualidade do código, mas a experiência com grandes projetos de software sugere um resultado diferente - produtividade reduzida e pouco ou nenhum aumento na qualidade do código."

De acordo com Anders Hejlsberg, havia um acordo bastante amplo em seu grupo de design de não ter verificado as exceções como um recurso de linguagem em C #. Hejlsberg explicou em uma entrevista que

“A cláusula throws, pelo menos da forma como é implementada em Java, não necessariamente força você a lidar com as exceções, mas se você não lidar com elas, ela o força a reconhecer precisamente quais exceções podem passar. Requer que você capture exceções declaradas ou as coloque em sua própria cláusula throws. Para contornar esse requisito, as pessoas fazem coisas ridículas. Por exemplo, eles decoram todos os métodos com "lança Exceção". Isso anula completamente o recurso, e você acabou de fazer o programador escrever mais gosma gobbledy. Isso não ajuda ninguém. ”

Visualizações sobre o uso

As exceções verificadas podem, em tempo de compilação , reduzir a incidência de exceções não tratadas que surgem em tempo de execução em um determinado aplicativo. Exceções não verificadas (como os objetos JavaRuntimeException e Error) permanecem sem tratamento.

No entanto, as exceções verificadas podem exigir throwsdeclarações extensas , revelando detalhes de implementação e reduzindo o encapsulamento , ou encorajar a codificação de blocos mal considerados que podem ocultar exceções legítimas de seus manipuladores apropriados. Considere uma base de código crescente ao longo do tempo. Uma interface pode ser declarada para lançar as exceções X e Y. Em uma versão posterior do código, se alguém quiser lançar a exceção Z, isso tornaria o novo código incompatível com os usos anteriores. Além disso, com o padrão do adaptador , no qual um corpo de código declara uma interface que é então implementada por um corpo de código diferente para que o código possa ser conectado e chamado pelo primeiro, o código do adaptador pode ter um rico conjunto de exceções para descreve problemas, mas é forçado a usar os tipos de exceção declarados na interface. try/catch

É possível reduzir o número de exceções declaradas declarando uma superclasse de todas as exceções potencialmente lançadas ou definindo e declarando tipos de exceção que são adequados para o nível de abstração do método chamado e mapeando exceções de nível inferior para esses tipos, de preferência empacotado usando o encadeamento de exceções para preservar a causa raiz. Além disso, é muito possível que, no exemplo acima da interface em mudança, o código de chamada também precise ser modificado, já que, de alguma forma, as exceções que um método pode lançar fazem parte da interface implícita do método.

Usar uma declaração ou geralmente é suficiente para satisfazer a verificação em Java. Embora isso possa ter alguma utilidade, essencialmente contorna o mecanismo de exceção verificado, que o Oracle desencoraja. throws Exceptioncatch (Exception e)

Os tipos de exceção não verificados geralmente não devem ser tratados, exceto possivelmente nos níveis mais externos do escopo. Estes geralmente representam cenários que não permitem a recuperação: s freqüentemente refletem defeitos de programação e s geralmente representam falhas irrecuperáveis ​​de JVM. Mesmo em um idioma que oferece suporte a exceções verificadas, há casos em que o uso de exceções verificadas não é apropriado. RuntimeExceptionError

Verificação dinâmica de exceções

O objetivo das rotinas de tratamento de exceções é garantir que o código possa tratar as condições de erro. A fim de estabelecer que as rotinas de tratamento de exceção são suficientemente robustas, é necessário apresentar o código com um amplo espectro de entradas inválidas ou inesperadas, como as que podem ser criadas por meio de injeção de falha de software e teste de mutação (que também é algumas vezes referido como fuzz teste ). Um dos tipos mais difíceis de software para o qual escrever rotinas de tratamento de exceção é o software de protocolo, uma vez que uma implementação de protocolo robusta deve ser preparada para receber entrada que não esteja em conformidade com a (s) especificação (ões) relevante (s).

Para garantir que uma análise de regressão significativa possa ser conduzida ao longo de um processo de ciclo de vida de desenvolvimento de software , qualquer teste de tratamento de exceção deve ser altamente automatizado e os casos de teste devem ser gerados de forma científica e repetível. Existem vários sistemas disponíveis comercialmente que realizam esses testes.

Em ambientes de mecanismo de tempo de execução, como Java ou .NET , existem ferramentas que se conectam ao mecanismo de tempo de execução e cada vez que ocorre uma exceção de interesse, eles registram informações de depuração que existiam na memória no momento em que a exceção foi lançada ( pilha de chamadas e heap valores). Essas ferramentas são chamadas de manipulação automatizada de exceções ou ferramentas de interceptação de erros e fornecem informações sobre a 'causa raiz' das exceções.

Sincronicidade de exceção

Algo relacionado com o conceito de exceções verificadas está a sincronicidade de exceção . As exceções síncronas acontecem em uma instrução de programa específica, enquanto as exceções assíncronas podem surgir em praticamente qualquer lugar. Segue-se que o tratamento de exceção assíncrona não pode ser exigido pelo compilador. Eles também são difíceis de programar. Exemplos de eventos naturalmente assíncronos incluem pressionar Ctrl-C para interromper um programa e receber um sinal como "parar" ou "suspender" de outro encadeamento de execução .

Linguagens de programação normalmente lidam com isso limitando a assincronicidade, por exemplo, Java descontinuou o uso de sua exceção ThreadDeath, que era usada para permitir que um encadeamento interrompesse outro. Em vez disso, pode haver exceções semi-assíncronas que surgem apenas em locais adequados do programa ou de forma síncrona.

Sistemas de condicionamento

Common Lisp , Dylan e Smalltalk têm um sistema de condição (consulte Common Lisp Condition System ) que abrange os sistemas de tratamento de exceções mencionados anteriormente. Nessas linguagens ou ambientes, o advento de uma condição (uma "generalização de um erro" de acordo com Kent Pitman ) implica uma chamada de função, e apenas mais tarde no manipulador de exceções a decisão de desfazer a pilha pode ser tomada.

As condições são uma generalização de exceções. Quando surge uma condição, um manipulador de condição apropriado é procurado e selecionado, em ordem de pilha, para lidar com a condição. Condições que não representam erros podem ser totalmente ignoradas com segurança; seu único propósito pode ser propagar dicas ou avisos para o usuário.

Exceções contínuas

Isso está relacionado ao chamado modelo de retomada do tratamento de exceções, no qual algumas exceções são ditas continuáveis : é permitido retornar à expressão que sinalizou a exceção, após ter executado uma ação corretiva no manipulador. O sistema de condição é generalizado assim: dentro do manipulador de uma condição não séria (também conhecida como exceção contínua ), é possível pular para pontos de reinicialização predefinidos (também conhecidos como reinicializações ) que estão entre a expressão de sinalização e o manipulador de condição. Reinicializações são funções fechadas em algum ambiente léxico, permitindo ao programador reparar este ambiente antes de sair completamente do manipulador de condição ou desenrolar a pilha, mesmo parcialmente.

Um exemplo é a condição ENDPAGE em PL / I; a unidade ON pode escrever linhas de trailer de página e linhas de cabeçalho para a próxima página e, em seguida, cair para retomar a execução do código interrompido.

Reinicia o mecanismo separado da política

Além disso, o tratamento de condições fornece uma separação entre o mecanismo e a política . As reinicializações fornecem vários mecanismos possíveis para a recuperação de erros, mas não selecione qual mecanismo é apropriado em uma determinada situação. Esse é o domínio do manipulador de condição, que (uma vez que está localizado no código de nível superior) tem acesso a uma visão mais ampla.

Um exemplo: suponha que haja uma função de biblioteca cujo objetivo é analisar uma única entrada do arquivo syslog . O que essa função deve fazer se a entrada estiver malformada? Não existe uma resposta certa, porque a mesma biblioteca pode ser implantada em programas para muitos propósitos diferentes. Em um navegador de arquivo de log interativo, a coisa certa a fazer pode ser retornar a entrada não analisada, para que o usuário possa vê-la - mas em um programa de resumo de log automatizado, a coisa certa a fazer pode ser fornecer valores nulos para o campos ilegíveis, mas aborta com um erro, se muitas entradas foram malformadas.

Ou seja, a pergunta só pode ser respondida em termos dos objetivos mais amplos do programa, que não são conhecidos pela função de biblioteca de uso geral. No entanto, sair com uma mensagem de erro raramente é a resposta certa. Portanto, em vez de simplesmente sair com um erro, a função pode estabelecer reinicializações oferecendo várias maneiras de continuar - por exemplo, para pular a entrada de log, fornecer valores padrão ou nulos para os campos ilegíveis, solicitar ao usuário os valores ausentes ou para desenrolar a pilha e abortar o processamento com uma mensagem de erro. Os reinícios oferecidos constituem os mecanismos disponíveis para a recuperação do erro; a seleção de reinicialização pelo manipulador de condição fornece a política .

Veja também

Referências

links externos