2009-12-02

Excepções, parte I (de códigos de erro a excepções)

Códigos de erro

Em C há muitas funções que devolvem valores especiais para indicar erros. Esta estratégia, embora simples, apresenta vários defeitos:

  1. o tipo de dados de retorno não pode ser usado por completo;
  2. o código que chama estas funções é bastante chato de escrever e difícil de ler (são necessário ifs por todo o lado);
  3. é fácil esquecermo-nos de testar os valores de retorno;
  4. é trabalhoso propagar estes erros manualmente pela stack acima;

Linguagens de mais alto nível possuem melhores mecanismos de notificação e tratamento de erros. Ou melhor, linguagens de mais alto nível possuem mecanismos de notificação e tratamento de erros. Muitas vezes esses mecanismos usam excepções.

Excepções

Uma excepção é uma forma de representar um erro. Em C++ podem ser de qualquer tipo de dados, mesmo simples inteiros. Mas, em linguagens de programação são objectos de um tipo específico de erros (chamado Exception ou Error por exemplo). Uma vantagem desta estratégia é que os erros são mais fáceis de identificar. Enquanto em C é necessário procurar os códigos de erro numa tabela, num sistema com excepções a própria excepção descreve o erro pelo seu nome (FileNotFoundException, DivideByZeroException, etc) e, para além disso, pode transportar mais informação, como a call stack quando ocorreu o erro, ou o nome do parâmetro responsável.

A diferença mais óbvia entre os mecanismos que usam excepções e a estratégia usada em C, é que as excepções não são resultados de funções/métodos. Como o próprio nome indica, as excepções representam situações excepcionais. Se um método devolve um valor não ocorreram erros. return usa-se para devolver resultados, não erros.

Quando ocorre um erro, cria-se um objecto que representa esse erro e “lança-se” essa excepção não com return mas com uma instrução usada especialmente para esse fim. Esta instrução é tipicamente chamada throw (comum em linguagens derivadas de C++) mas por vezes também raise (em Python e Ruby).

Quando um método lança uma excepção, significa que ocorreu um erro que este não consegue tratar. Então passa a responsabilidade ao método que o chamou (caller). Se este também não puder tratar o erro, a responsabilidade é passada para quem o chamou este. E assim sucessivamente, até o erro ser tratado, ou a excepção chegar ao topo da stack, onde geralmente temos um método/função chamado main. Se a excepção chegar ao topo da stack sem ser tratada o programa é terminado e geralmente é apresentada um mensagem de erro “feia” ao utilizador.

Em C esta passagem de responsabilidade é manual. Todos os métodos tem de propagar os erros manualmente pela stack acima. Com excepções, se o caller não tratar explicitamente erro a excepção borbulha sozinha pela stack acima (inglês: bubbles up the stack).

Recursos

Quando devolvemos códigos de erro em C surgem problemas se tivermos de libertar recursos. Por exemplo:

int* badFunction(size_t x)
{
int* stuff = (int*) malloc(x);
int val1 = mayError1("foo", stuff);
if(val1 == -1)
{
return NULL;
}
if(mayError2(val1, stuff) == -1)
{
return NULL;
}
return stuff;
}

Quando ocorre um erro em mayError1 ou mayError2 é necessário libertar a memória que foi alocada em stuff. Podíamos simplesmente adicionar uma linha em cada if com free(stuff);. Mas e se mais tarde alterassemos a função para abrir um ficheiro também? Tínhamos de fechar o ficheiro em ambos os ifs. E se o resultado de mayError1 fosse um recurso que tinha de ser libertado antes da função terminar? Tínhamos de libertar esse recurso no segundo if e antes do return do resultado correcto. Esta situação pode ficar rapidamente dificil de controlar, especialmente se adicionarmos outros tipos de recurso que pode necessitar de ser libertado, como locks. Por vezes nestas situações encontram-se usos esporádicos de goto numa tentativa de imitar os mecanismos das linguagens que usam excepções.

No entanto se usarmos excepções tudo isto é mais simples.

try-catch-finally-whatever

Há vários tratamentos possíveis a dar a uma excepção:

  1. identificar e corrigir o problema: na maior parte das vezes, não é possível;
  2. deixar propagar a excepção para cima na stack: quando este método não pode fazer nada acerca do erro;
  3. substituir a excepção por uma que faça mais sentido para quem chamou este método, e passá-la para cima;
  4. limpar recursos (fechar ficheiros, fechar ligações, libertar memória, libertar locks, etc): quando este método adquiriu recursos que são da sua responsabilidade;

Quando um método A chama um método B que pode lançar excepções, as linguagens permitem o tratamento dessas excepções graças a blocos geralmente chamados try. Coloca-se o código que pode produzir erros num bloco try, e depois usam-se outros blocos especiais para tratar dos erros. Uma estrutura comum é try-catch-finally (comum em linguagens derivadas de C++ como Java e C#). Algumas linguagens usam nomes diferentes: Python usa try-except-finally, Ruby usa begin-rescue-ensure, mas a ideia é a mesma.

No bloco catch/except/rescue define-se um filtro para as excepções que queremos tratar, e o código desse bloco irá correr quando essa excepção surgir no bloco try. Ao usar catch a excepção pára de ser propagada pela stack acima. Este bloco usa-se para a primeira, e para a terceira situações apresentadas acima.

O bloco finally/ensure corre sempre no final do bloco try, independentemente de surgirem erros ou não. Este bloco usa-se para executar código para limpar recursos (situação 4 acima). Geralmente este bloco corre mesmo que o bloco termine com um return, break, continue ou outra instrução de salto (goto, ugh!). Este bloco usa-se para a quarta situação.

Algumas linguagens disponibilizam mecanismos adicionais para tratar excepções. Em Python existe um outro bloco, chamado else (try-except-else-finally) que corre sempre que não surjam erros no try. Em Ruby existe a instrução retry que, quando usada num bloco rescue, faz com que o bloco try seja executado novamente. Em C# existe a instrução using que serve para adquirir recursos e libertá-los implicitamente sem ter de o fazer manualmente com finally. O equivalente em Python é with.

A seguir

No próximo post vou falar dos vários tipos de erro que podem surgir e quando se deve ou não usar excepções para os tratar.

0 commentários:

Enviar um comentário