Tratamento de exceção

Origem: Wikipédia, a enciclopédia livre.
Ir para: navegação, pesquisa

O tratamento de exceção, na ciência da computação, é o mecanismo responsável pelo tratamento da ocorrência de condições que alteram o fluxo normal da execução de programas de computadores. Para condições consideradas parte do fluxo normal de execução, ver os conceitos de sinal e evento.

Em geral, na ocorrência de uma exceção, o estado do programa é gravado em um local pré-definido e a sua execução é direcionada para uma rotina de tratamento. Dependendo da situação, a rotina de tratamento pode prosseguir a execução a partir do ponto que originou a exceção, utilizando a informação gravada para restaurar o estado. Um exemplo de exceção que normalmente permite o prosseguimento da execução é aquela originada de uma falta de página (page fault). Por outro lado, uma . Do ponto de vista do processador, as interrupções de hardware podem ser consideradas como condições análogas às exceções que permitem prosseguimento da execução, apesar destas exceções não serem normalmente relacionadas com o fluxo normal do programa.

Para o desenvolvedor de uma rotina, lançar uma exceção é um modo útil de assinalar que a rotina não deve continuar a execução quando, por exemplo, os argumentos de entrada não são válidos (um denominador igual a zero em uma divisão, por exemplo) ou quando um recurso no qual o programa depende não está disponível (um arquivo não encontrado ou um erro em um disco, por exemplo). Em sistemas que não utilizam o conceito de exceções as rotinas devem retornar algum código de erro especial. Porém esta abordagem é muitas vezes difícil de implementar devido ao problema do predicado, onde os usuários da rotina precisam escrever código extra para distinguir os valores de retorno normais daqueles que indicam erro.

Aplicações seguras[editar | editar código-fonte]

Uma porção de código é considerada "segura", no contexto do tratamento de exceções, se as falhas no código, ocorridas em tempo de execução, não produzem efeitos prejudiciais, como vazamentos de memória, corrupção de dados ou saída inválida. Um código seguro deve satisfazer invariantes mesmo após a ocorrência de exceções. Os níveis de segurança relacionados ao tratamento de exceção podem ser colocados da seguinte forma:

  1. transparência à falha : há garantia que as operações ocorrerão com sucesso e satisfarão todos os requerimentos, mesmo na presença de situações excepcionais. Este é o melhor nível de segurança.
  2. transacional : as operações podem falhar, mas quando isso ocorre as operações não causam efeitos colaterais.[1]
  3. segurança básica: execuções parciais de operações que falham podem causar efeitos colaterais, mas os invariantes de estado são preservados (isto é, qualquer dado gravado conterá valores válidos).
  4. segurança mínima: execuções parciais de operações que falham podem gravar dados inválidos mas não levarão à falha completa (crash) do programa.
  5. sem segurança: não há qualquer garantia. Este é o pior nível de segurança para com exceções.

Normalmente um nível básico de segurança é requerido. A transparência à falhas é difícil de implementar e normalmente não é possível de atingir em bibliotecas onde o conhecimento completo da aplicação não está disponível.

Suporte a tratamento de exceções em linguagens de programação[editar | editar código-fonte]

Muitas linguagens de programação, como Ada, Object Pascal, C++, D, Delphi, Eiffel, Java, Objective-C, OCaml, PHP (versão 5), Python, REALbasic, ML, Ruby, e todas linguagens do framework .NET contém suporte nativo para tratamento de exceção. Nestas linguagens, no advento de uma exceção (mais precisamente, uma exceção tratada pela linguagem), a pilha de execução é varrida até que uma rotina de tratamento de exceção seja encontrada. Isto é, se a função f, que contém uma rotina de tratamento H para a exceção E, chama a função g, que por sua vez chama a função h, e a exceção E ocorre em h, então a função h e g irão terminar em H se f tratar E.

Existem apenas alguns poucos estilos de tratamento de exceção em uso, que contém pequenas diferenças entre si. No estilo mais popular, uma exceção é iniciada por uma declaração especial (throw ou raise) com um objeto representando a exceção (como em Java ou Object Pascal, por exemplo) ou um valor de um tipo de enumeração extensível (como em Ada). O escopo de uma rotina de tratamento de exceção é iniciada com uma cláusula de marcação (try ou a palavra reservada da linguagem para início de bloco, como begin), e termina com o início do bloco de tratamento (catch, except ou rescue). Várias cláusulas indicando blocos de tratamento para diferentes tipos de exceções podem ser colocados na sequência.

Algumas poucas linguagens também permitem o uso da cláusula (else) que é utilizada em caso de exceção ocorrida antes do final do escopo do bloco de tratamento ser alcançado. Mais comum é a utilização da cláusula finally (ou ensure) que é executada independente da ocorrência da exceção, tipicamente para liberar os recursos obtidos no corpo do bloco de tratamento de exceções. A linguagem C++ não possui tal cláusula e, como alternativa, a técnica de Resource Acquisition Is Initialization pode ser utilizada para liberar os recursos.

O código de tratamento de exceções pode parecer como o mostrado abaixo (em pseudo-código):

try {
    line = console.readLine();
    if (line.length() == 0) {
        throw new EmptyLineException("A linha lida da console está vazia!");
    }
    console.printLine("Alô %s!" % line);
} catch (EmptyLineException e) {
    console.printLine("Alô!");
} catch (Exception e) {
    console.printLine("Erro: " + e.message());
} else {
    console.printLine("O programa executou com sucesso.");
} finally {
    console.printLine("O programa termina neste momento.");
}

Algumas linguagens utilizam uma única cláusula de tratamento, que trata internamente a classe da exceção.

As linguagens Perl e C não usam o termo "tratamento de exceção", mas incluem facilidades que permitem a implementação de uma funcionalidade similar.

Exceções verificadas[editar | editar código-fonte]

Os projetistas da linguagem Java desenvolveram um conjunto especial de exceções[2] [3] [4] : as exceções verificadas (checked exceptions)[5] . As exceções verificadas que um método pode lançar devem fazer parte da sua assinatura. Por exemplo, se um método pode lançar a exceção IOException, ele precisa declarar este fato na sua assinatura, senão um erro em tempo de compilação é assinalado.

Este mecanismo é relacionado com os verificadores de exceção que existem no OCaml (uma implementação da linguagem Caml). A ferramenta externa para o OCaml é transparente (não requer qualquer anotação sintática) e facultativa (é possível compilar e executar uma aplicação sem verificar as exceções, apesar disto não ser recomendável para código utilizado em produção).

A linguagem de programação CLU possui uma funcionalidade com interface próxima ao que foi introduzido posteriormente pela linguagem Java. Uma função pode lançar apenas as exceções listadas no seu tipo, mas qualquer exceção não listada, lançada quando da chamada de uma função, pode ser automaticamente transformada em uma exceção de tempo de execução (runtime), ou falha, ao invés de resultar em um erro em tempo de compilação. Mais tarde, a linguagem Modula-3 incorporou uma funcionalidade similar[6] . Estas funcionalidades não incluem a verificação em tempo de compilação, que é central ao conceito de exceções verificadas e que, até o ano de 2006, não haviam sido incorporadas nas principais linguagens de programação, com exceção da linguagem Java.[7]

Prós e contras[editar | editar código-fonte]

As exceções verificadas podem, em tempo de compilação, reduzir consideravelmente (mas não eliminar inteiramente) a ocorrência de casos em que de exceções não tratadas emergem para fora da aplicação; as exceções não verificadas (RuntimeExceptions e Errors) podem permanecer sem tratamento.

Porém, alguns criticam as exceções verificadas alegando que esta sintaxe requer assinaturas de métodos com extensas declarações de cláusula throws, que freqüentemente revelam detalhes da implementação interna e reduzem o encapsulamento, ou ainda encorajam o abuso de blocos try/catch mal desenhados, que podem esconder a ocorrência de exceções das rotinas que deveriam tratá-las.[8] Além disso, o uso destas exceções pode dificultar a manutenção dos programas. Por exemplo, uma interface pode ser inicialmente declarada para lançar as exceções X e Y mas, numa versão posterior do programa, percebe-se que um novo tipo de erro pode ser detectado, e é necessário, em adição, lançar a exceção Z, provocando uma mudança na interface que fará do código cliente incompatível com a nova interface.[8]

Outros consideram que os problemas citados podem ser resolvidos se o número de exceções declaradas for reduzida através da utilização de uma superclasse que generalize todas as classes de exceções potencialmente lançadas ou pela definição e declaração de tipos de exceção adequadas para o nível de abstração correspondente ao método chamado[9] através do mapeamento de exceções de nível mais baixo a estes tipos, preferencialmente envolvendo estas exceções através do uso do mecanismo de encadeamento de exceções com o objetivo de preservar a exceção raiz.

Um simples declaração throws Exception ou catch (Exception e) é sempre suficiente para satisfazer a verificação. Enquanto esta técnica é algumas vezes útil, ela efetivamente engana o mecanismo de exceções verificadas, e portanto deve ser utilizada com cuidado.

Uma visão predominante considera que as exceções não verificadas não devem ser tratadas, com exceção do nível mais externo do escopo do programa, pois elas frequentemente representam cenários em que a recuperação não é possível: as exceções de RuntimeException normalmente representam defeitos de programação[9] ou situações de erro (Error) que indicam problemas irrecuperáveis da JVM. Isto significa que, em resumo, mesmo em linguagens que suportam exceções verificadas, existem cenários em que a sua utilização não é apropriada.

Sincronicidade de exceções[editar | editar código-fonte]

A sincronicidade de exceções é relacionada de certa forma com o conceito das exceções verificadas. As exceções síncronas ocorrem em declarações específicas de um programa enquanto que exceções assíncronas podem ser lançadas em praticamente qualquer lugar.[10] [11] Além disso o tratamento das exceções assíncronas não pode ser requerido pelo compilador e o seu tratamento é difícil de programar. Como exemplos de exceções naturalmente assíncronas pode-se citar o evento decorrente do ato de pressionar as teclas Ctrl-C com o intuito de interromper um programa.

Tipicamente, as linguagens lidam com este problema limitando a "assincronicidade". Por exemplo, a linguagem Java perdeu as características que permitiam a parada e reinicialização da execução de threads perdidas, para evitar que objetos fiquem em um estado inconsistente.[12]

Sistemas baseados em condições[editar | editar código-fonte]

As linguagens Common Lisp, Dylan e Smalltalk possuem um sistema de condições que englobam as funcionalidades dos sistemas de tratamento de exceções. Nestas linguagens ou ambientes o advento de uma condição ("a generalização de um erro" segundo Kent Pitman) implica uma chamada de função, e somente no escopo da subrotina de tratamento de exceção a decisão de varrer a pilha de execução é tomada.

As condições podem ser entendidas como a generalização das exceções. Quando uma condição surge, uma subrotina apropriada de tratamento de condições é procurada e selecionada, na ordem da pilha, para tratar a condição. As condições que não representam erros podem ser ignoradas com segurança; o seu único propósito pode ser a propagação de alertas para o usuário.[13]

Exceções "continuáveis"[editar | editar código-fonte]

As exceções "continuáveis" são relacionadas com o modelo conhecido como "modelo do recomeço" de tratamento de exceções, na qual algumas exceções são ditas como "continuáveis"; permitem que a execução retorne ao ponto que originou a exceção, após a subrotina de tratamento de exceções ter tomado as ações corretivas necessárias. O sistema de condições é generalizado da seguinte forma: dentro da subrotina de tratamento de condições consideradas não sérias (exceções "continuáveis" ou, em inglês, continuable exception), é possível saltar para pontos de reinicio pré-definidos (restarts) que residem entre a expressão que sinalizou a exceção e a subrotina de tratamento da condição.

Referências

  1. Matt Austern. Standard Library Exception Policy (em inglês). Visitado em 12 de julho de 2007.
  2. Ann Wollrath. A mailing list for discussion of JavaSoft's Remote Method Invocation (em inglês). Visitado em 12 de julho de 2007.
  3. The origin of checked exceptions (em inglês). Visitado em 12 de julho de 2007.
  4. Rod Waldhoff. Java's checked exceptions were a mistake (and here's what I would like to do about it) (em inglês). Visitado em 12 de julho de 2007.
  5. Sun Microsystems. Java Language Specification, chapter 11.2. (em inglês). Visitado em 12 de julho de 2007.
  6. Blair MacIntyre. Modula 3 - Writing Programs (em inglês). Visitado em 12 de julho de 2007.
  7. Bruce Eckel. Does Java need Checked Exceptions? (em inglês). Visitado em 12 de julho de 2007.
  8. a b Brian Goetz. Java theory and practice: The exceptions debate (em inglês). Visitado em 12 de julho de 2007.
  9. a b Joshua Block. Effective Java Programming Language Guide. [S.l.]: Addison-Wesley Professional, 2001. ISBN 0201310058.
  10. Simon Marlow, Simon Peyton Jones, Andrew Moran, John Reppy. Asynchronous Exceptions in Haskell (em inglês). Visitado em 12 de julho de 2007.
  11. Stephen N. Freund, Mark P. Mitchell. Safe asynchronous exceptions for Python (em inglês). Visitado em 12 de julho de 2007.
  12. Sun Microsystems. Why Are Thread.stop, Thread.suspend, Thread.resume and Runtime.runFinalizersOnExit Deprecated? (em inglês). Visitado em 12 de julho de 2007.
  13. Franz Inc.. ANSI Common Lisp - Condition System Concepts (em inglês). Visitado em 12 de julho de 2007.

Ligações externas[editar | editar código-fonte]