2009-12-08

Excepções, parte II (vários tipos de erros)

No último post falei de excepções e das instruções fornecidas por linguagens de alto-nível para as manipular. Neste vou falar das situações em que estas surgem e como devem ser tratadas.

Tipos de erro

Os erros que podem surgir numa aplicação podem ser divididos em quatro categorias, consoante com a sua causa: erros do programador, erros do utilizador, erros de ambiente e o que eu costumo chamar erros de fim do mundo.

Erros do programador

Alguns erros podem ser previstos e prevenidos antes de ocorrerem. Este é o caso clássico da divisão por zero. Embora 1/0 resulte em DivideByZeroException (ou semelhante), a excepção pode ser evitada com um simples if. É melhor prevenir o erro do que corrigí-lo depois da sua ocorrência. Em vez de:

int x;
try
{
x = dividend / divisor
}
catch (DivideByZeroException e)
{
x = 1;
}

devemos escrever:

if (divisor == 0)
x = 1;
else
x = dividend / divisor;

Um outro exemplo comum de um erro do programador é um excepção por tentar aceder a referências nulas (NullReferenceException em .NET, NullPointerException em Java). Também neste caso é preferível evitar o uso da referência nula em vez de tratar a excepção.

Este tipo de erros deve ser prevenido, mas não corrigido depois da sua ocorrência (em runtime). Isto significa que as excepções que os representam nunca devem ser lançadas, nem ser explicitamente capturadas com catch. Qualquer pedaço de código com catch(NullReferenceException) ou qualquer outra excepção que indique erros deste género está mal. Este tipo de erros são bugs, não são condições excepcionais.

Mas se estas excepções nunca devem ser lançadas, porque é que elas existem? Porque as linguagens permitem as situações nas quais elas podem surgir. Nessas linguagens cabe ao programador evitar que elas surjam. Não faz sentido Java proibir a divisão porque pode ser por zero, ou proibir chamar métodos porque a referência pode ser nula. No entanto, compiladores mais inteligentes podem detectar algumas situações onde isso acontece. Por exemplo:

Object o = null;
String s = o.toString(); // NPE!

Este código está obviamente errado, mas a maioria dos compiladores não se queixam.

Erros do utilizador

Um outro tipo de erros que pode ocorrer são erros do utilizador. Quando este introduz um valor alfanúmerico num campo númerico, por exemplo. Estes erros não pode ser prevenidos, mas podem ser detectados atempadamente, por isso nestes casos também não é justificável usar uma excepção. Em lugar de:

try
{
int value = int.Parse(input);
}
catch(FormatException e)
{
UI.Message("Wrong value");
}

É melhor escrever:

int value;

if (!int.TryParse(input, out value))
{
UI.Message("Are you blind? No letters!");
return;
}

Esta sintaxe funciona porque C# tem passagem de parâmetros por referência. Numa linguagem que permite mais que um valor de retorno podemos usar uma forma semelhante. Senão temos de recorrer a outras estratégias. Um hipotético método TryParse numa linguagem sem parâmetros por referência como Java, teria de devolver o que seria de esperar de um método que tenta converter uma String para int, ou seja, o resultado da conversão, que pode ser sucesso ou falha:

int value;
ParseResult result = tryParseInt(input);
if (result.success()) {
value = result.get();
}
else {
UI.message("Are you blind? No letters!");
return;
}

Independentemente das capacidades da linguagem, o que interessa reter é que, quando estamos a tentar converter uma string para um número, não se deve lançar uma excepção quando tal não é possível. O resultado de uma tentativa de converter uma string com letras para um número não é um erro, é uma conversão falhada. As excepções representam casos excepcionais. Não há nada de excepcional em o utilizador cometer erros.

Erros de ambiente

Há erros não podem ser previstos nem evitados. Este tipo de erros surge devido ao ambiente externo que o programa usa e sobre o qual não tem controlo nenhum (o sistema de ficheiros, a rede, etc).

É o caso de abrir um ficheiro que não existe:

if(!File.Exists(path))
{
UI.Message("The file is gone!");
}
else
{
var stream = File.Open(path);
// whatever
}

Embora parece que estamos a evitar que a excepção ocorra, não é o caso. Neste código existe uma race condition. Embora possamos verificar se um ficheiro existe antes de o abrir, não temos garantias que ele não é apagado por outro processo entre a verificação se ele existe e a tentativa de o abrir.

Como estes erros não podem ser previstos nem evitados, temos de usar as excepções se os quisermos tratar (com catch). Vou falar mais destas no próximo post.

Erros de fim do mundo

Estes erros também não podem ser previstos nem prevenidos e a sua fonte também é o ambiente externo do programa. No entanto este tipo de erros tem uma razão de ser diferente dos que classifiquei como erros de ambiente.

Quando ocorre um erro de ambiente o programa pode perfeitamente continuar. No exemplo dado de ler um ficheiro e não existir, em vez de tentar lê-lo podemos pedir ao utilizador para abrir outro, por exemplo. Mas há erros que não nos permitem continuar a execução como, por exemplo, ficar sem memória (aka out of memory), chegar ao limite da stack (aka stack overflow), ou aceder a áreas de memória inválidas/não permitidas (aka access violation ou segmentation fault). Numa situação destas não há absolutamente nada que possamos fazer para os corrigir, por isso não faz sentido nenhum usar catch(OutOfMemoryException).

Este tipo de erros costuma indicar bugs (memory leaks, recursão infinita, apontadores perdidos, etc).

EDIT (19-05-2010):

Por vezes, tentar salvar o que quer que seja depois deste tipo de erros pode ser pior do que não fazer nada. Por exemplo, quando surge uma access violation/segmentation fault, significa que alguns apontadores/referências não estão a apontar para os sítios certos. Ao tentar corrigir isto podemos estar a escrever em locais de memória completamente aleatórios, ou pior, exactamente onde um atacante quer...

Sumário

Para sumarizar aqui fica uma tabela com as principais diferenças:

Causas do erro

Pode ser prevenido?

Pode ser previsto?

Frequência de ocorrência esperada

Deve ser tratado (com try-catch-finally)?

programador

sim

sim

nunca

nunca

utilizador

não

sim

frequente

não

ambiente

não

não

ocasional

sempre

fim do mundo

por vezes

não

raramente

não

1 commentários:

JoseCid disse...

Bom post como sempre!

Espero ansiosamente por Sockets, continua a carregar!

Enviar um comentário